├── Linux ├── Linux常用命令.md └── Linux内核.md ├── 算法 ├── 分治法(Divide Conquer).md ├── 回溯法(Backtracking).md ├── media │ ├── 排序算法 │ │ ├── 分类.png │ │ ├── 比较.png │ │ ├── 冒泡排序.gif │ │ ├── 基数排序.gif │ │ ├── 堆排序.gif │ │ ├── 希尔排序.gif │ │ ├── 归并排序.gif │ │ ├── 快速排序1.gif │ │ ├── 快速排序2.jpg │ │ ├── 插入排序.gif │ │ ├── 计数排序.gif │ │ └── 选择排序.gif │ └── 动态规划 │ │ └── 多段图问题.jpg ├── 双指针(Two Pointers).md ├── 二分查找(Binary Search).md ├── 动态规划(Dynamic Programming).md ├── 贪心算法(Greedy).md └── 排序算法(Sort).md ├── 数据结构 ├── media │ ├── 树 │ │ ├── B+树.png │ │ ├── B树.png │ │ ├── 字典树.jpg │ │ ├── 红黑树.jpg │ │ ├── B+树插入.gif │ │ └── B树插入.gif │ ├── 图论 │ │ ├── 邻接矩阵.jpg │ │ ├── 邻接表.png │ │ └── Dijkstra算法示例.gif │ └── 堆 │ │ ├── 插入操作.jpg │ │ └── 移除操作.jpg ├── 并查集(Union Find).md ├── 哈希(Hash).md ├── 堆(Heap).md ├── 栈(Stack)和队列(Queue).md ├── 数组(Array)和链表(LinkedList).md ├── 图(Graph).md └── 树(Tree).md ├── Java ├── media │ └── JVM │ │ └── 双亲委派模型.jpg ├── Java基础.md ├── Java开源框架.md ├── Java集合.md ├── Java并发.md └── JVM.md ├── 操作系统 ├── media │ └── 进程管理 │ │ ├── 进程的三态模型.jpg │ │ ├── 进程的五态模型.jpg │ │ └── 进程队列模型.jpg ├── I-O管理.md ├── 文件管理.md ├── 内存管理.md └── 进程管理.md ├── 基于多核的并行编程 ├── media │ ├── 并行编程基础 │ │ ├── 并发.jpg │ │ └── 并行.jpg │ ├── 并行计算模型与系统 │ │ ├── MIMD.jpg │ │ ├── SIMD.jpg │ │ ├── SISD.jpg │ │ ├── MPP结构图.jpg │ │ ├── PVP结构图.jpg │ │ ├── SMP结构图.jpg │ │ ├── 内存访问模型分类.jpg │ │ ├── 多级存储体系结构.jpg │ │ ├── 多节点系统架构.jpg │ │ └── Cluster结构图.jpg │ ├── OpenMP多核编程 │ │ ├── 操作符和初始值.jpg │ │ ├── 消除循环的数据依赖.jpg │ │ └── Fork-Join模型.jpg │ └── Linux多核编程 │ │ └── Joinable与Detached线程.jpg ├── Ch0 课程简介.md ├── Ch1 并行编程基础.md ├── Ch1-2 并行编程基础.md ├── Ch1-1 并行计算模型与系统.md ├── Ch2-3 OpenMP多核编程.md ├── Ch2-1 Windows多核编程.md └── Ch2-2 Linux多核编程.md ├── 计算机网络 ├── media │ ├── 计算机网络参考模型 │ │ ├── TCP段.png │ │ ├── UDP段.png │ │ ├── 以太网帧.jpg │ │ ├── IP数据报.png │ │ └── 不同模型对比.jpg │ ├── OSI第四层:传输层 │ │ ├── 快恢复算法.jpeg │ │ ├── 快重传算法.jpeg │ │ ├── 慢开始算法.jpg │ │ ├── 滑动窗口机制.jpg │ │ ├── 四次握手释放链接.jpg │ │ ├── 拥塞避免算法.jpeg │ │ └── TCP三次握手建立连接.jpg │ └── OSI第五~七层:应用层 │ │ ├── HTTP响应报文结构.jpg │ │ └── HTTP请求报文结构.png ├── OSI第三层:网络层(Network).md ├── 计算机网络参考模型.md ├── OSI第四层:传输层(Transport).md └── OSI第五~七层:应用层(Application).md ├── README.md ├── 数据库 ├── Redis.md ├── MySQL.md └── 基础知识.md └── 软件设计 └── 软件设计.md /Linux/Linux常用命令.md: -------------------------------------------------------------------------------- 1 | # Linux常用命令 2 | 3 | -------------------------------------------------------------------------------- /算法/分治法(Divide Conquer).md: -------------------------------------------------------------------------------- 1 | # 分治法(Divide Conquer) 2 | 3 | ## 概述 4 | 5 | ## 常见应用 6 | 7 | -------------------------------------------------------------------------------- /算法/回溯法(Backtracking).md: -------------------------------------------------------------------------------- 1 | # 回溯法(Backtracking) 2 | 3 | ## 概述 4 | 5 | ## 常见应用 6 | 7 | -------------------------------------------------------------------------------- /数据结构/media/树/B+树.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/树/B+树.png -------------------------------------------------------------------------------- /数据结构/media/树/B树.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/树/B树.png -------------------------------------------------------------------------------- /数据结构/media/树/字典树.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/树/字典树.jpg -------------------------------------------------------------------------------- /数据结构/media/树/红黑树.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/树/红黑树.jpg -------------------------------------------------------------------------------- /算法/media/排序算法/分类.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/分类.png -------------------------------------------------------------------------------- /算法/media/排序算法/比较.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/比较.png -------------------------------------------------------------------------------- /数据结构/media/图论/邻接矩阵.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/图论/邻接矩阵.jpg -------------------------------------------------------------------------------- /数据结构/media/图论/邻接表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/图论/邻接表.png -------------------------------------------------------------------------------- /数据结构/media/堆/插入操作.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/堆/插入操作.jpg -------------------------------------------------------------------------------- /数据结构/media/堆/移除操作.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/堆/移除操作.jpg -------------------------------------------------------------------------------- /数据结构/media/树/B+树插入.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/树/B+树插入.gif -------------------------------------------------------------------------------- /数据结构/media/树/B树插入.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/树/B树插入.gif -------------------------------------------------------------------------------- /算法/media/动态规划/多段图问题.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/动态规划/多段图问题.jpg -------------------------------------------------------------------------------- /算法/media/排序算法/冒泡排序.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/冒泡排序.gif -------------------------------------------------------------------------------- /算法/media/排序算法/基数排序.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/基数排序.gif -------------------------------------------------------------------------------- /算法/media/排序算法/堆排序.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/堆排序.gif -------------------------------------------------------------------------------- /算法/media/排序算法/希尔排序.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/希尔排序.gif -------------------------------------------------------------------------------- /算法/media/排序算法/归并排序.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/归并排序.gif -------------------------------------------------------------------------------- /算法/media/排序算法/快速排序1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/快速排序1.gif -------------------------------------------------------------------------------- /算法/media/排序算法/快速排序2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/快速排序2.jpg -------------------------------------------------------------------------------- /算法/media/排序算法/插入排序.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/插入排序.gif -------------------------------------------------------------------------------- /算法/media/排序算法/计数排序.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/计数排序.gif -------------------------------------------------------------------------------- /算法/media/排序算法/选择排序.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/算法/media/排序算法/选择排序.gif -------------------------------------------------------------------------------- /Java/media/JVM/双亲委派模型.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/Java/media/JVM/双亲委派模型.jpg -------------------------------------------------------------------------------- /操作系统/media/进程管理/进程的三态模型.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/操作系统/media/进程管理/进程的三态模型.jpg -------------------------------------------------------------------------------- /操作系统/media/进程管理/进程的五态模型.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/操作系统/media/进程管理/进程的五态模型.jpg -------------------------------------------------------------------------------- /操作系统/media/进程管理/进程队列模型.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/操作系统/media/进程管理/进程队列模型.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行编程基础/并发.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行编程基础/并发.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行编程基础/并行.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行编程基础/并行.jpg -------------------------------------------------------------------------------- /数据结构/media/图论/Dijkstra算法示例.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/数据结构/media/图论/Dijkstra算法示例.gif -------------------------------------------------------------------------------- /计算机网络/media/计算机网络参考模型/TCP段.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/计算机网络参考模型/TCP段.png -------------------------------------------------------------------------------- /计算机网络/media/计算机网络参考模型/UDP段.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/计算机网络参考模型/UDP段.png -------------------------------------------------------------------------------- /计算机网络/media/计算机网络参考模型/以太网帧.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/计算机网络参考模型/以太网帧.jpg -------------------------------------------------------------------------------- /计算机网络/media/OSI第四层:传输层/快恢复算法.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第四层:传输层/快恢复算法.jpeg -------------------------------------------------------------------------------- /计算机网络/media/OSI第四层:传输层/快重传算法.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第四层:传输层/快重传算法.jpeg -------------------------------------------------------------------------------- /计算机网络/media/OSI第四层:传输层/慢开始算法.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第四层:传输层/慢开始算法.jpg -------------------------------------------------------------------------------- /计算机网络/media/OSI第四层:传输层/滑动窗口机制.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第四层:传输层/滑动窗口机制.jpg -------------------------------------------------------------------------------- /计算机网络/media/计算机网络参考模型/IP数据报.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/计算机网络参考模型/IP数据报.png -------------------------------------------------------------------------------- /计算机网络/media/计算机网络参考模型/不同模型对比.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/计算机网络参考模型/不同模型对比.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/MIMD.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/MIMD.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/SIMD.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/SIMD.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/SISD.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/SISD.jpg -------------------------------------------------------------------------------- /计算机网络/media/OSI第四层:传输层/四次握手释放链接.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第四层:传输层/四次握手释放链接.jpg -------------------------------------------------------------------------------- /计算机网络/media/OSI第四层:传输层/拥塞避免算法.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第四层:传输层/拥塞避免算法.jpeg -------------------------------------------------------------------------------- /基于多核的并行编程/media/OpenMP多核编程/操作符和初始值.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/OpenMP多核编程/操作符和初始值.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/MPP结构图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/MPP结构图.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/PVP结构图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/PVP结构图.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/SMP结构图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/SMP结构图.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/内存访问模型分类.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/内存访问模型分类.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/多级存储体系结构.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/多级存储体系结构.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/多节点系统架构.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/多节点系统架构.jpg -------------------------------------------------------------------------------- /计算机网络/media/OSI第四层:传输层/TCP三次握手建立连接.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第四层:传输层/TCP三次握手建立连接.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/OpenMP多核编程/消除循环的数据依赖.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/OpenMP多核编程/消除循环的数据依赖.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/并行计算模型与系统/Cluster结构图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/并行计算模型与系统/Cluster结构图.jpg -------------------------------------------------------------------------------- /计算机网络/media/OSI第五~七层:应用层/HTTP响应报文结构.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第五~七层:应用层/HTTP响应报文结构.jpg -------------------------------------------------------------------------------- /计算机网络/media/OSI第五~七层:应用层/HTTP请求报文结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/计算机网络/media/OSI第五~七层:应用层/HTTP请求报文结构.png -------------------------------------------------------------------------------- /基于多核的并行编程/media/OpenMP多核编程/Fork-Join模型.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/OpenMP多核编程/Fork-Join模型.jpg -------------------------------------------------------------------------------- /基于多核的并行编程/media/Linux多核编程/Joinable与Detached线程.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CroffZ/InterviewCollection/HEAD/基于多核的并行编程/media/Linux多核编程/Joinable与Detached线程.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 面试资料整理 2 | 3 | - [x] Java 4 | - [x] 数据结构 5 | - [ ] 算法 6 | - [ ] 操作系统 7 | - [x] 计算机网络 8 | - [x] 数据库 9 | - [ ] Linux 10 | - [x] 软件设计 11 | - [x] 基于多核的并行编程 12 | 13 | -------------------------------------------------------------------------------- /操作系统/I-O管理.md: -------------------------------------------------------------------------------- 1 | # I/O管理 2 | 3 | ## 输入输出(I/O)管理 4 | 5 | ### I/O管理概述 6 | ### I/O设备 7 | ### I/O管理目标 8 | ### I/O管理功能 9 | ### I/O应用接口 10 | ### I/O控制方式 11 | 12 | ## I/O核心子系统 13 | 14 | ### I/O调度概念 15 | ### 高速缓存与缓冲区 16 | ### 设备分配与回收 17 | ### 假脱机技术(SPOOLing) 18 | ### 出错处理 19 | 20 | -------------------------------------------------------------------------------- /算法/双指针(Two Pointers).md: -------------------------------------------------------------------------------- 1 | # 双指针(Two Pointers) 2 | 3 | ## 概述 4 | 在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向或者相反方向的指针进行扫描,从而达到相应目的。 5 | 6 | ## 常见应用 7 | * 给定一个有序递增数组,在数组中找到满足条件的两个数,使得这两个数的和为某一给定的值。如果有多对数,只输出一对即可。 8 | * Hoare的双向扫描快速划分法 9 | * 奇偶排序 10 | * 单链表中的环:快慢指针 11 | * 单链表求中间元素:快慢指针 12 | 13 | 14 | -------------------------------------------------------------------------------- /算法/二分查找(Binary Search).md: -------------------------------------------------------------------------------- 1 | # 二分查找(Binary Search) 2 | 3 | ## 查找流程 4 | 1. 排序:将数据集有序排列。 5 | 2. 数据分半:将排序好的数据集切分成大致相等的两份。 6 | 3. 查找数据:因为有排序,查找的时候先和其中一半数据集中的最大或最小的数进行比较来断定要查找的数据是否会包含在其中,然后在满足判定条件的数据集里,重复执行以上过程,直到找到数据或者比较完所有数据返回没有该数据。 7 | 8 | ## 时间复杂度 9 | * 包括排序:O(nlog(n)) + O(log(n)) = O(nlog(n)) 10 | * 不包括排序:O(log(n)) 11 | 12 | -------------------------------------------------------------------------------- /基于多核的并行编程/Ch0 课程简介.md: -------------------------------------------------------------------------------- 1 | # Ch0 课程简介 2 | 3 | ## 摩尔定律 4 | * 提出人:英特尔(Intel)创始人之一,戈登·摩尔(Gordon Moore)。 5 | * 内容:当价格不变时,集成电路上可容纳的晶体管数目,约每隔18个月便会增加一倍。 6 | 7 | ## 课程内容 8 | * 并行计算基础:系统、模型、算法与编程方法 9 | * 多核计算概述:多核架构与多核处理器、多核编程 10 | * 基于多核的并行编程 11 | * 多线程与并行编程 12 | * 多线程编程:Windows多线程编程、Linux进程与pthread 13 | * OpenMP、MPI 14 | * TBB、Erlang等(多进程编程、进程间通信) 15 | * 并行编程中常见问题及解决方法 16 | * Intel多核处理器的软件工具 17 | 18 | -------------------------------------------------------------------------------- /数据结构/并查集(Union Find).md: -------------------------------------------------------------------------------- 1 | # 并查集(Union Find) 2 | 3 | ## 并查集 4 | * 用集合中的某个元素来代表这个集合,该元素称为集合的代表元。 5 | * 一个集合内的所有元素组织成以代表元为根的树形结构。 6 | * 对于每一个元素,parent[x]指向x在树形结构上的父亲节点。如果x是根节点,则parent[x] = x。 7 | * 对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元,则可以沿着parent[x]不断在树形结构中向上移动,直到到达根节点。 8 | * 判断两个元素是否属于同一集合,只需要看他们的代表元是否相同即可。 9 | 10 | ## 路径压缩 11 | 为了加快查找速度,查找时将x到根节点路径上的所有点的parent设为根节点。使用该优化后,平均复杂度可视为是一个常数。 12 | 13 | ## 常见应用 14 | 15 | -------------------------------------------------------------------------------- /操作系统/文件管理.md: -------------------------------------------------------------------------------- 1 | # 文件管理 2 | 3 | ## 文件系统基础 4 | 5 | ### 文件概念 6 | ### 文件结构 7 | * 顺序文件 8 | * 索引文件 9 | * 索引顺序文件 10 | 11 | ### 目录结构 12 | * 文件控制块和索引节点 13 | * 单级目录结构和两级目录结构 14 | * 树形目录结构 15 | * 图形目录结构 16 | 17 | ### 文件共享 18 | * 共享动机 19 | * 共享方式 20 | * 共享语义 21 | 22 | ### 文件保护 23 | * 访问类型 24 | * 访问控制 25 | 26 | ### 文件系统基础 27 | ### 文件系统实现 28 | ### 文件系统层次结构 29 | ### 目录实现 30 | ### 文件实现 31 | 32 | ## 磁盘组织与管理 33 | 34 | ### 磁盘的结构 35 | ### 磁盘调度算法 36 | ### 磁盘的管理 37 | 38 | -------------------------------------------------------------------------------- /数据结构/哈希(Hash).md: -------------------------------------------------------------------------------- 1 | # 哈希(Hash) 2 | 3 | ## 哈希函数 4 | * 哈希用于将任意长度的数据映射到固定长度的数据。 5 | * 如果不同的主键得到相同的哈希值,则发生冲突。 6 | 7 | ## 冲突解决 8 | * 链地址法(Separate Chaining):在链地址法中,每个桶(bucket)是相互独立的,每一个索引对应一个元素列表。处理HashMap的时间就是查找桶的时间(常量)与遍历列表元素的时间之和。 9 | * 开放地址法(Open Addressing):在开放地址方法中,当插入新值时,会判断该值对应的哈希桶是否存在,如果存在则根据某种算法依次选择下一个可能的位置,直到找到一个未被占用的地址。开放地址即某个元素的位置并不永远由其哈希值决定。 10 | 11 | ## 重哈希(Rehash) 12 | * 当元素越来越多的时候,冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在扩容之后,需要把原数组中的数据必须重新计算其在新数组中的位置并放进去。 13 | * 发生时机:当元素个数超过数组大小*负载因子(LoadFactor)时,就会进行数组扩容。负载因子的默认值为0.75,扩容默认扩大一倍。 14 | * 扩容之后会重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。 15 | 16 | ## 常见应用 17 | 18 | -------------------------------------------------------------------------------- /基于多核的并行编程/Ch1 并行编程基础.md: -------------------------------------------------------------------------------- 1 | # Ch1 并行编程基础 2 | 3 | ## 并发和并行 4 | * 并发(Concurrency):多个线程或事件在同一时间段被执行,以时间片划分的机制同时运行多个程序,但在任意一个时刻只有一个线程在执行。 5 | ![并发](media/并行编程基础/并发.jpg) 6 | * 并行(Parallel):多个线程或事件在同一时刻被执行,需要多个处理器核心。 7 | ![并行](media/并行编程基础/并行.jpg) ## 扩展性(Scalability) * 计算机资源(包括硬件、软件资源)为了适应增加的性能和功能需求而向上扩展(scale up)或为了减少开销而向下扩展(scale down)。 8 | * 具体指以下方面的扩展性:功能与性能,成本开销控制,兼容性。 9 | * 扩展性的维度: 10 | * 资源扩展性:机器可以根据需要增减资源。 11 | * 应用扩展性:可以增减功能。 12 | * 技术扩展性:现有架构不变的情况下,能否适应新技术。 ## 加速比(speedup) 13 | * 定义:最优串行算法执行时间/并行程序执行时间 14 | * 作用:描述对程序并行化之后获得的性能收益。 15 | * Amdahl定律 16 | * 固定问题规模(工作负载),减少响应时间。 17 | * 加速比=1/(S+(1-S)/n),其中S是指程序中串行部分的比例,n是指机器规模(或处理器核的数目)。 18 | * Gustafson定律 19 | * 固定计算时间,扩大问题规模,提高计算精度。 20 | * 加速比=S+(1-S)*n,其中S是指程序中串行部分的比例,n是指机器规模(或处理器核的数目)。 21 | 22 | 23 | -------------------------------------------------------------------------------- /操作系统/内存管理.md: -------------------------------------------------------------------------------- 1 | # 内存管理 2 | 3 | ## 内存管理基础 4 | 5 | ### 内存管理概念 6 | * 程序装入与链接 7 | * 逻辑地址与物理地址空间 8 | * 内存保护 9 | 10 | ### 交换与覆盖 11 | ### 连续分配管理方式 12 | * 单一连续分配:每个进程占用一个物理上完全连续的存储空间。 13 | * 固定分区分配:把内存分成数量和大小固定的分区,记录每个分区被哪个进程使用。 14 | * 可变分区分配:按进程的内存需求来动态划分分区。 15 | 16 | ### 可变分区的内存分配算法 17 | * 最先适应分配算法 18 | * 邻近适应分配算法 19 | * 最优适应分配算法 20 | * 最坏适应分配算法 21 | 22 | ### 非连续分配管理方式 23 | * 分页管理方式:将内存空间分成若干大小相同的页,分配给进程使用,同一程序分配到的页可能不连续,使用页表记录程序页与内存页的对应关系。 24 | * 分段管理方式:将程序的地址空间划分为若干段,每个段整段存储在内存中,使用段表记录程序每个段的起始地址。 25 | * 段页式管理方式:将程序的地址空间先分成若干个段,再将每段分成若干个大小相等的页。内存空间也分成大小相等的页,以页为单位分配给程序。 26 | 27 | ## 虚拟内存管理 28 | 29 | ### 虚拟内存基本概念 30 | ### 请求分页管理方式 31 | ### 页面置换算法 32 | * 最佳置换算法(OPT) 33 | * 先进先出置换算法(FIFO) 34 | * 最近最少使用置换算法(LRU) 35 | * 时钟置换算法(CLOCK) 36 | 37 | ### 页面分配策略 38 | ### 抖动现象、工作集 39 | ### 请求分段管理方式 40 | ### 请求段页式管理方式 41 | 42 | -------------------------------------------------------------------------------- /数据结构/堆(Heap).md: -------------------------------------------------------------------------------- 1 | # 堆(Heap) 2 | 3 | ## 堆 4 | * 堆是一种基于二叉树的数据结构,又被称为优先队列(Priority Queue)。Java中,PriorityQueue的底层数据结构就是堆(默认是最小堆)。 5 | * 整个堆中的所有父子节点的键值都满足相同的排序条件,分为最大堆和最小堆。 6 | * 时间复杂度 7 | * 索引:O(log(n)) 8 | * 查找:O(log(n)) 9 | * 插入:O(log(n)) 10 | * 删除:O(log(n)) 11 | * 删除最大值/最小值:O(1) 12 | 13 | ## 最大堆和最小堆 14 | * 最大堆:父节点的键值永远大于等于所有子节点的键值,根节点的键值是最大的。 15 | * 最小堆:父节点的键值永远小于等于所有子节点的键值,根节点的键值是最小的。 16 | 17 | ## 实现方式 18 | * 数组(普遍):对于每一个下标为`i`的节点(i从0开始): 19 | * 其左子节点在下标为`2*i+1`的位置; 20 | * 其右子节点在下标为`2*i+2`的位置; 21 | * 其父节点在下标为`(i-1)/2`的位置。 22 | * 二叉树(直观) 23 | 24 | ## 相关操作 25 | 26 | ### 插入 27 | 插入操作是将一个元素插入到集合中,首先把该元素放集合尾部,然后执行上浮操作,如下图示例。 28 | ![插入操作](media/堆/插入操作.jpg) 29 | 30 | ### 移除 31 | 移除操作时,在集合非空情况下移除集合中第一个元素,也就是下标为0的元素,然后将集合中最后一个元素移到下标为0位置,在将下标为0的新元素执行下沉操作,如下图示例。 32 | ![移除操作](media/堆/移除操作.jpg) 33 | 34 | ## 常见应用 35 | 36 | ### 找最大的N个数 37 | 将开始的N个数建立size为N的小根堆,后续数据如果小于根结点,直接抛弃;如果大于根结点,则把根节点(最小的数)删除,然后插入新的数。 38 | 39 | -------------------------------------------------------------------------------- /数据结构/栈(Stack)和队列(Queue).md: -------------------------------------------------------------------------------- 1 | # 栈(Stack)和队列(Queue) 2 | 3 | ## 栈(Stack) 4 | * 栈是一个元素集合,是一种后进先出的数据结构(LIFO),支持两种基本操作:push用于将元素压入栈,pop用于删除栈顶元素。 5 | * 时间复杂度 6 | * 索引:O(n) 7 | * 查找:O(n) 8 | * 插入:O(1) 9 | * 删除:O(1) 10 | 11 | ## 队列(Queue) 12 | * 队列是一个元素集合,是一种先进先出的数据结构(FIFO),支持两种基本操作:enqueue用于添加一个元素到队列,dequeue用于删除队列中的一个元素。 13 | * 时间复杂度 14 | * 索引:O(n) 15 | * 查找:O(n) 16 | * 插入:O(1) 17 | * 删除:O(1) 18 | 19 | ## 常见应用 20 | 21 | ### 一个数组实现两个栈 22 | * 算法一:将数组的两头分别作为两个栈的栈顶指针,插入数据时向中间靠拢,指针相遇说明空间已满,需对数组进行扩容。 23 | * 算法二:将数组的中间两个数据位置分别作为两个栈的栈顶指针,插入数据时向两边走,其中有一个指针走到边界说明空间已满,则需对数组进行扩容。(缺点:有极端情况,一个栈满了但另一个栈很空,仍需扩容,空间利用率低) 24 | * 算法三:将数组的奇偶位置分别作为两个栈的栈顶指针,插入数据时各自向前走, 其中有一个指针走到右边界说明空间已满,则需对数组进行扩容。(缺点:有极端情况,一个栈满了但另一个栈很空,仍需扩容,空间利用率低) 25 | 26 | ### 两个栈实现一个队列 27 | * 入队时直接压入栈A;出队时先把栈A的所有元素依次出栈后压入栈B,此时栈B的顶部就是原先栈A的底部元素,即队列要求出队的元素。 28 | * 出队操作后如果还要入队,则还要把栈B的所有元素依次出栈后压入栈A,即还原栈A。 29 | 30 | ### 两个队列实现一个栈 31 | * 压栈时直接入队A;出栈时,把队A的元素依次出队后入队B,除了最后一个元素。 32 | * 最后一个元素就是出栈要求的那个元素,将它返回即可。 33 | * 出栈一次后,栈A和栈B的代号互换(栈A变为栈B,栈B变为栈A)。 34 | 35 | ### 实现一个栈,使其入栈、出栈、获取最小元素三个操作的时间复杂度均为O(1) 36 | * 维护两个栈,分别称为S和Min,S用来进行正常的栈操作,Min用来存放最小元素。 37 | * 入栈:先入S栈,然后比较新元素和Min栈顶的元素,如果新元素小于Min栈顶的元素,则入Min栈。 38 | * 出栈:先出S栈,如果要出栈的元素在Min栈的栈顶,则也需要把Min栈的栈顶元素出栈。 39 | * 获取最小元素:获取Min栈顶的元素。 40 | * 类似地,也可以实现最大元素栈。 41 | 42 | ### 排序栈s1中元素,只能使用一个额外的辅助栈s2 43 | 1. s1中的第一个栈顶元素直接弹出到s2中,因为一个元素不需要考虑顺序问题; 44 | 2. 当s1非空时,每次弹出一个栈顶元素保存在临时变量temp中,将s2中小于temp全部弹出到s1中; 45 | 3. temp入栈s2; 46 | 4. 把第(2)步中从s2弹出到s1中的所有元素再返回到s2,实际上这一步可以省略,因为s2新的栈顶比这几个元素都要大,因此s1现在的栈顶至少包括这几个元素比新栈顶小,根据第(2)步的判断也会同样弹回s2中。 47 | 5. 重复以上(2)-(4)步,直到s1为空时,所有元素在s2中已经完成了降序排列,弹回s1即完成了升序排列。 48 | 49 | 50 | -------------------------------------------------------------------------------- /Linux/Linux内核.md: -------------------------------------------------------------------------------- 1 | # Linux内核 2 | 3 | ## 进程 4 | 5 | ### fork进程的操作 6 | * 调用fork创建的子进程,将共享父进程的代码空间,复制父进程数据空间,此时子进程会获得父进程的所有变量的一份拷贝。 7 | * fork()返回值意义如下: 8 | * =0:在子进程中,表示当前进程是子进程。 9 | * >0:在父进程中,返回值为子进程的id值(唯一标识号)。 10 | * -1:创建失败。 11 | * fork的特点是:一次调用,两次返回。 12 | 13 | ### 父子进程、孤儿进程和僵尸进程 14 | * 通过fork函数创建的新进程是原进程的子进程,而调用fork函数的进程是fork函数创建出来的新进程的父进程。也就是说,通过fork函数创建的新进程与原进程是父子关系,fork就相当于一个凭证,有fork,就有父子关系。 15 | * 当父进程结束了而子进程未结束时,子进程就变成了孤儿进程,交由init进程管理。init进程是一个守护进程,在大多数Linux系统中init进程的pid为1,不过Ubuntu为了减轻pid为1进程的压力,将其内核修改过,所以Ubuntu中的init进程pid不为1。因为init进程管理了很多孤儿进程,所以也称之为进程的孤儿院。 16 | * Unix提供了一种机制:在每个进程退出的时候,内核释放该进程所有的资源,但是仍然为其保留一定的信息(包括pid、退出状态、运行时间等),直到父进程通过wait/waitpid来取时才释放。但这样就导致了问题,如果父进程不调用wait/waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,这就是僵尸进程。但是系统所能使用的进程号是有限的,如果产生了大量僵尸进程,就会因为没有可用的进程号而导致系统不能产生新的进程。 17 | * 任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。 18 | * 父进程死后,僵尸进程会变成孤儿进程,过继给init进程,init进程始终会负责清理僵尸进程,它产生的所有僵尸进程也跟着消失。 19 | 20 | ### fork和vfork的区别 21 | * fork要拷贝父进程的数据段;而vfork则不需要完全拷贝父进程的数据段。在子进程没有调用exec和exit之前,子进程与父进程共享数据段。 22 | * fork不限制父子进程的执行次序;而vfork调用时,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制。 23 | 24 | ## 信号(Signal) 25 | 26 | ### 概念 27 | * 信号是系统中对进程的通知。比如控制台中按Ctrl+C可以停下当前正在执行的进程,还有kill命令可以强制中止对应pid的进程。 28 | * 是Linux下处理异步事件的一种方式,名称全部以SIG开头。 29 | * 本质上是一种软中断。 30 | 31 | ### 信号的产生 32 | * 终端特殊按键:Ctrl+C(SIGINT) 33 | * 硬件异常中断信号,如内存错误(SIGSEGV) 34 | * kill(1) 35 | * kill(2) 36 | 37 | ### Linux中的信号 38 | 39 | ### Signal信号的可靠性 40 | * 不一定每个信号都能被捕捉到。 41 | * 信号不会被阻塞,如果发信号时系统刚好卡了一下,信号就会被卡没了。 42 | * 信号处理有自动复位机制。避免方法:在信号处理函数中再注册一次自身。但是这样仍然是不可靠的,原因是进入处理函数到重新注册之间有时间差,在这段时间差内的信号不会被信号处理函数捕捉到并处理。 43 | 44 | -------------------------------------------------------------------------------- /数据结构/数组(Array)和链表(LinkedList).md: -------------------------------------------------------------------------------- 1 | # 数组(Array)和链表(LinkedList) 2 | 3 | ## 数组(Array) 4 | * 数组是一个线性元素集合,每个元素有自己的序号。 5 | * 时间复杂度 6 | * 索引:O(1) 7 | * 查找:O(1) 8 | * 插入:O(1) 或 O(n) 9 | * 删除:O(1) 或 O(n) 10 | 11 | ## 链表(LinkedList) 12 | * 链表是一种由节点(Node)组成的线性数据集合,每个节点通过指针指向下一个节点。它是一种由节点组成,并能用于表示序列的数据结构。 13 | * 单链表:每个节点仅指向下一个节点,最后一个节点指向null。 14 | * 双链表:每个节点有两个指针,一个指针指向前一个节点,另一个指针指向下一个节点;第一个节点的前指针和最后一个节点的后指针指向null。 15 | * 循环链表:每个节点指向下一个节点,最后一个节点指向第一个节点。 16 | * 时间复杂度: 17 | * 索引:O(n) 18 | * 查找:O(n) 19 | * 插入:O(1) 20 | * 删除:O(1) 21 | * 跳表(SkipList):在有序的单链表中,可以建立一个多级索引结构,把表中某些元素提取出来作为索引,在查找时可以先从索引查起,相当于用空间换时间。 22 | 23 | ## 常见应用 24 | 25 | ### 单链表反转并倒序输出 26 | * 从后到前遍历并修改指针和输出元素。 27 | * 使用递归或栈来实现。 28 | 29 | ### 合并两个有序的单链表成为一个新的有序单链表 30 | 1. 维护两个指针分别指向原先的两个链表,初始指向两个链表头。维护一个指针指向新链表的尾部。 31 | 2. 比较两个指针指向的项,取出其中较小(大)的一个放入新链表,相应指针往后移动。 32 | 3. 当其中一个指针走到结尾时,直接把另一个指针剩下未处理的元素全部添加到新链表的尾部。 33 | 34 | ### 找出单链表指定位置的元素 35 | * 倒数第K个元素:快指针比慢指针先走K步,然后快慢指针每次各走一步。快指针走到结尾时,慢指针就指向倒数第K个元素。 36 | * 中间元素:快指针每次走两步,慢指针每次走一步。快指针走到结尾时,慢指针就指向中间元素。 37 | 38 | ### O(1)时间复杂度删除节点 39 | * 题目:给定头节点和指定节点,要求删除这个节点。 40 | * 思路:把要删除的节点的下一个节点的值移动到要删除的节点上,然后把下一个节点删除。 41 | 42 | ### O(1)时间复杂度添加节点 43 | * 题目:给定新节点和指定节点,要求把新节点插入到指定节点之前。 44 | * 思路:把新节点和指定节点的值交换,题目就转换成把新节点插入到指定节点之后。 45 | 46 | ### 单链表中的环 47 | * 判断是否有环:慢指针每次走一步,快指针每次走两步。如存在环,则两者相遇;如不存在环,快指针走到结尾时退出。 48 | * 计算环的长度:从快慢指针的相遇点开始,绕着环走一圈,再次走到相遇点的位置时,所走过的结点数就是环的长度。 49 | * 找出环的入口:两个指针分别从相遇点和起点开始走,再次相遇的点就是环的入口。 50 | * 以上问题也可以用Hash法解决,以空间换时间。 51 | 52 | ### 两个单链表相交问题 53 | * 两个单链表都没有环: 54 | * 方法一:暴力遍历两个链表。 55 | * 方法二:先遍历第一个链表,建立哈希表,再遍历第二个链表,判断每个节点地址哈希值是否和第一个表中的节点地址值有相同,即可判断两个链表是否相交,相同的点就是相交的点。 56 | * 方法三:先遍历第一个链表到其尾部,然后将尾部的原本指向NULL的next指针指向第二个链表头,这样两个链表就合成了一个链表,问题就转变为判断新的链表是否有环,环的入口就是相交的点。 57 | * 方法四:一旦两个链表相交,那么两个链表从相交节点开始到尾节点一定都是相同的节点。所以,如果他们相交的话,那么他们最后的一个节点一定是相同的,因此分别遍历到两个链表的尾部,然后判断两个尾节点是否相同。 58 | * 一个有环一个没有:肯定不相交。 59 | * 两个单链表都有环: 60 | * 不相交:两个链表有各自的环。 61 | * 相交的点在环内(包括环入口):交点为两个链表的环入口,所以只需要找到两个链表的环的入口就可以了。 62 | * 相交的点在环外:计算出两个链表的长度之差,记为K,然后让长链表的指针先走K步,短链表的指针再开始走,这样他们一定同时到达第一个公共节点。只需要在向后移动的时候比较两个链表的节点是否相等就可以获得第一个公共节点。 63 | 64 | -------------------------------------------------------------------------------- /算法/动态规划(Dynamic Programming).md: -------------------------------------------------------------------------------- 1 | # 动态规划(Dynamic Programming) 2 | 3 | ## 概述 4 | * 动态规划法也用于求解最优化问题,也采用分步决策的策略,将一个大问题划分成若干个较小的同类子问题,根据子问题的解,自底向上,得出整个问题的解。 5 | * 与贪心法的异同 6 | * 相同:都是用于求解最优化问题;都采用分步决策,计算出每一步的最优解。 7 | * 不同:贪心法的每一步决策依赖于『最优量度标准』,不依赖于子问题的解和尚未作出的选择;动态规划法每一步决策依赖于子问题的解,无需最优量度标准。 8 | * 与分治法的异同 9 | * 相同:都将问题话分成若干个规模较小的同类型子问题。 10 | * 不同:分治法会有重叠子问题的现象,对于一些子问题会重复计算,而动态规划法能避免重叠子问题现象。 11 | 12 | ## 最优子结构 13 | * 动态规划法具有最优子结构特性。 14 | * 最优子结构特性:一个问题的最优解包含其子问题的最优解。 15 | * 当一个问题具有最优子结构特性时,在构造该问题最优解的过程中,只需考虑每一个子问题的最优解。因为每个子问题的最优解构成了该问题的最优解。 16 | 17 | ## 常见应用 18 | 19 | ### 最短路径算法 20 | * 题目描述:给定一个带权有向图,计算任意两结点间的最短路径。 21 | * 分析:Dijkstra算法可以计算从指定点到所有结点的最短路径长度,因此分别对每个结点使用一次迪杰斯特拉算法即可求的任意两个结点间的最短路径。迪杰斯特拉算法的时间复杂度为O(n^2 ),因此采用这种方法的时间复杂度为O(n^3 )。但是,迪杰斯特拉算法不允许权值为负数,因此需要使用弗洛伊德算法。弗洛伊德算法允许权值为负数的边,但不允许回路的路径长度为负数。因为,若回路长度为负数,那么走一次回路,路径长度一定比上一次小,故这个问题就没有意义了。 22 | * 参考:图(Graph)。 23 | 24 | ### 0/1背包问题 25 | * 题目描述:有一个背包,最多放Mkg的物体(物体大小不限);有n个物体,每个物体的重量为Wi,每个物体只可以选择放进背包或不放进背包,放进背包后可获得收益Pi。问:如何放置能获得最大的收益? 26 | * 注:背包问题分为两种,若每个物体可以切分,则称为一般背包问题,可以使用贪心法求得最优解;若每个物体不可分割,则称为0/1背包问题,这种问题用动态规划解决。这里讨论的是0/1背包问题。 27 | * 问题分析:设f(i,m)表示第i步时背包的总收益,其中i表示当前进行到了第i步,m为当前背包载重。第i步有两种选择: 28 | * 将第i个物体放入背包,此时背包总收益就变成f(i-1,m-Wi)+Wi。 29 | * 第i个物体不放入背包,此时背包总收益就是f(i-1,m)。 30 | * 第i步究竟怎么选择,就取决于这两种选择中哪个结果更大,因此要分别计算者两种情况的值,选较大者作为第i步的结果。 31 | * 根据数学归纳法可知,如果从第一步开始,每一步选择的都是最优选择,那么最终可以得到最优解。 32 | 33 | ### 多段图问题 34 | * 题目描述:给定一个多段图,求出多段图中的最短路径和最短路径长度。 35 | * 注:多段图是一个有向、无环、带权图;它有且仅有一个起始结点和一个终止结点;它有n个阶段,每个阶段由特定的几个结点构成;每个结点的所有结点都只能指向下一个相邻的阶段,阶段之间不能越界。如图: 36 | ![多段图问题](media/动态规划/多段图问题.jpg) 37 | * 题目分析 38 | * 从前往后依次给所有结点编号,序号从0开始,依次递增,同一阶段的结点顺序可以随意。 39 | * 创建数组cost,用于记录以某个结点为起点,到终点的最短路径长度值,数组的下标表示结点的编号,数组的值表示以该结点为起点,到终点的最短路径长度。 40 | * 创建数组path,用于记录最短路径中出现的所有结点,数组的下标表示结点的编号,数组的值表示结点的后继结点编号。 41 | * 算法流程: 42 | 1. 从最后一个结点开始,从后向前,依次计算每个结点的cost值和path值; 43 | 2. 对于结点i,找到其所有的出边,假设出边的终点分别是a、b、c,边上的权值分别是w1、w2、w3,分别计算w1+cost[a]、w2+cost[b]、w3+cost[c],将其中最小的那个值作为cost[i];并将最小的那个后继结点作为d[i]。 44 | 3. 直到将所有结点都计算完毕后,即可得到最短路径。 45 | 46 | ### 最长公共子序列 47 | * 题目描述:给定两个序列,求出它们的最长公共子序列。如:序列X={a,b,c,b,d,a,b},Y={b,d,c,a,b,a},则X和Y的最长公共子序列为{b,c,b,a}。 48 | * 注意:子序列为原序列的一个子集,并不要求连续,但要求子序列中元素的顺序和原序列元素的顺序一致。 49 | 50 | -------------------------------------------------------------------------------- /算法/贪心算法(Greedy).md: -------------------------------------------------------------------------------- 1 | # 贪心算法(Greedy) 2 | 3 | ## 求解思路 4 | * 既然贪心法用于解决最优化问题,所以我们首先对问题进行数学建模,找出其中的:目标函数、约束条件。 5 | * 最优化问题的结果需要用一个n元组来表示,如X=(x1,x2,x3,……,xn)。贪心法的执行一共需要n步,每一步都会确定n元组中的一个元素,并保证每一步选取的值都是局部最优的。在经过n步之后,一共选取了n个值,每个值都是局部最优的,最终我们就可以认为这n个局部最优的值是整体最优的。 6 | * 那么,在每一步中,究竟通过怎样的策略来选取一个当前局部最优解呢?这个选取策略就叫做『最优量度标准』(也叫做贪心准则)。最优量度标准选择的好坏,直接影响最终的结果是不是整体最优。而最优量度标准的选择往往是根据经验来确定的,也就是并不是所有的最优量度标准都能达到整体最优。所以你选取的那个最优量度标准能否导致整体最优,这是需要额外证明的。 7 | 8 | ## 使用条件 9 | * 要求解的问题是一个最优化问题; 10 | * 这个问题的解可以用n元组表示; 11 | * 该问题满足最优子结构特性; 12 | * 可以找到最优量度标准,并可以证明该最优量度标准能导致一个整体最优解。 13 | * PS:并非对所有最优化问题都能找到最优量度标准,若找不到可以使用动态规划法。 14 | 15 | ## 常见应用 16 | 17 | ### 一般背包问题 18 | * 题目描述:有一个背包,最多放Mkg的物体(物体大小不限);有n个物体,每个物体的重量为Wi,每个物体完全放入背包后可获得收益Pi。问:如何放置能获得最大的收益? 19 | * 注:背包问题分为两种,若每个物体不可分割,则称为0/1背包问题,这种问题无法用贪心法求得最优解,只能求的近似解;而若每个物体可以切分,则称为一般背包问题,可以使用贪心法求的最优解。这里讨论的是一般背包问题。 20 | * 结果集:一般背包问题中,结果集可以用一个n元组表示:x的下标i表示物体的序号;xi表示第i个物体加入背包的部分(0<=xi<=1) 21 | * 目标函数:背包收益的最大值(xi与Pi的乘积和)最大 22 | * 约束条件:物体总重量(xi与Wi的乘积和)不能超过背包容量 23 | * 量度标准: 24 | * 标准1:重量小的物体优先。将所有物体按照重量递增的顺序排序,每次选重量最小的放入背包。这个量度标准显然无法得到整体最优解,因为重量小的物体并不一定价值高。最优解与价值、重量这两个维度产生关系,而这个量度标准仅考虑了一个维度,因此这样选择并不能导致整体最优解。 25 | * 标准2:价值高的物体优先。这种选法也无法达到整体最优解,理由同上。 26 | * 标准3:性价比高的物体优先。首先计算所有物体的性价比(价值和重量的比值),每次优先将性价比高的物体放入背包。这种量度标准考虑了两个维度,可以得到整体最优解。 27 | 28 | ### 最佳合并模式 29 | * 题目描述:给定n个有序文件,每个文件的记录数分别为wi,请给出一种两两合并的方案,使得总合并次数最少。 30 | * 注意: 31 | * 外排序算法是将多个有序文件合并成一个有序文件的过程。 32 | * 在一次合并的过程中,两个文件中的所有记录都需要先从文件中读入内存,再在内存中排序,最后将排序的结果写入文件中。 33 | * 假设两个待排序文件记录数分别为n、m,那么将这两个文件合并成一个有序的文件需要进行n+m次读写。 34 | * 问题转化: 35 | * n个文件两两合并的过程可以用一棵扩充二叉树来表示。因为扩充二叉树只有度为2或0的节点,没有度为1的节点,这符合两两合并的过程。 36 | * 在扩充二叉树中叶节点表示原始文件,非叶结点表示合并过程中的文件,节点的权值表示文件的记录数。 37 | * n个文件合并过程的总读写次数为带权外路径长度之和,因此,问题就转化为『如何求扩充二叉树的最小加权路径』,可以用哈夫曼算法解决。 38 | * 思路:若要使得带权外路径长度最小,可以将权值大的节点尽量靠近根节点,这样路径短一些;而权值小的节点可以适当远离根节点,因为权值小,外路径稍微长一点也没事。 39 | * 实现步骤: 40 | 1. 用一个优先队列存储所有的初始节点; 41 | 2. 从队列中选出两个权值最小的节点,将它们的和作为它们的根节点,并放入队列中; 42 | 3. 循环这个过程,直到队列中只有一个节点为止,此时具有最小带权路径的扩充二叉树构造完毕!此时带权外路径长度即为最小的读写次数。 43 | 44 | ### 最小代价生成树 45 | * 题目描述:n个村庄间架设通信线路,每个村庄间的距离不同,如何架设最节省开销? 46 | * 问题分析:这个问题中,村庄可以抽象成节点,村庄之间的距离抽象成带权值的边,要求最节约的架设方案其实就是求如何使用最少的边、最小的权值和将图中所有的节点连接起来。这就是一个最小代价生成树的问题,可以用Prim算法或kruskal算法解决。 47 | * 参考:图(Graph)。 48 | 49 | ### 单源最短路径 50 | * 题目描述:给一个有向无环带权图,并给一个起点,求出该原点到所有顶点的最短路径。 51 | * 问题分析:使用Dijkstra算法。 52 | * 参考:图(Graph)。 53 | 54 | ### 跳格子 55 | * 题目描述:给一个非负整数数组,数组里的每个元素表示从该位置可以跳出的最远距离,要求问从第一个元素(index=0)开始,能否达到数组的最后一个元素。 56 | * 问题分析:所以这里可以使用贪心算法,计算出到某个点时能够跳出的最大距离(当前的最大值和(当前点+能跳出的最大距离)的较大的值),如果能跳出的最大距离大于最后一个点的位置,那么能到达最后一个元素;如果到达当前点后,不能在往后跳,那么不能达到最后点,返回false(通常是当前点的数字为0且前面的点所能到达的最大值点就是当前点)。 57 | 58 | 59 | -------------------------------------------------------------------------------- /计算机网络/OSI第三层:网络层(Network).md: -------------------------------------------------------------------------------- 1 | # OSI第三层:网络层(Network) 2 | 3 | ## 网络层的功能 4 | * 在网络中传输数据。 5 | * 采用分级寻址。 6 | * 将网络分段,并且控制流量。 7 | * 减少网络拥堵。 8 | * 与其他网段通信。 9 | 10 | ## IP地址 11 | * 32位二进制数字,由网段号和主机号组成。 12 | * 5类地址: 13 | 14 | | 类别 | 格式 | 号段 | 私有地址空间 | 15 | | --- | --- | --- | --- | 16 | | A类地址 | 0+7位网络号+24位主机号 | 0~127 | 10.0.0.0~10.255.255.255 | 17 | | B类地址 | 10+14位网络号+16位主机号 | 128~191 | 172.16.0.0~172.31.255.255 | 18 | | C类地址 | 110+21位网络号+8位主机号 | 192~223 | 192.168.0.0~192.168.255.255 | 19 | | D类地址 | 1110+20位网络号+8位主机号 | 224~239 | 无 | 20 | | E类地址 | 1111+20位网络号+8位主机号 | 240~255 | 无 | 21 | > 其中,D类地址用于多播,E类地址用于研究。 22 | > 网段号是指主机号全为0,广播地址是主机号全为1。 23 | 24 | ## 子网和子网掩码 25 | * 子网是指从主机号段借位来继续划分网段,可借位数最小为2,最大为主机号位数-2。 26 | * 子网掩码用来决定网段号和主机号分别为多少位,与IP地址做按位AND运算。 27 | * A类地址:255.0.0.0 28 | * B类地址:255.255.0.0 29 | * C类地址:255.255.255.0 30 | 31 | ## 交换机和路由器的区别 32 | 33 | ### 二层交换机 34 | * 二层交换机是数据链路层的设备,它能够读取数据包中的MAC地址信息并根据MAC地址来进行交换。交换机内部有一个地址表,这个地址表标明了MAC地址和交换机端口的对应关系。 35 | * 当交换机从某个端口收到一个数据包,它首先读取包头中的源MAC地址,这样它就知道源MAC地址的机器是连在哪个端口上的,它再去读取包头中的目的MAC地址,并在地址表中查找相应的端口,如果表中有与这目的MAC地址对应的端口,则把数据包直接复制到这端口上,如果在表中找不到相应的端口则把数据包广播到所有端口上,当目的机器对源机器回应时,交换机又可以学习一目的MAC地址与哪个端口对应,在下次传送数据时就不再需要对所有端口进行广播了。 36 | * 二层交换机一般都含有专门用于处理数据包转发的ASIC (Application Specific Integrated Circuit)芯片,因此转发速度可以做到非常快。 37 | 38 | ### 路由器 39 | * 路由器是网络层的设备,它内部有一个路由表,标明了如果要去某个地方,下一步应该往哪走。 40 | * 路由器从某个端口收到一个数据包,它首先把链路层的包头去掉(拆包),读取目的IP地址,然后查找路由表,若能确定下一步往哪送,则再加上链路层的包头(打包),把该数据包转发出去;如果不能确定下一步的地址,则向源地址返回一个信息,并把这个数据包丢掉。 41 | * 路由技术其实是由两项最基本的活动组成,即决定最优路径和传输数据包。其中,数据包的传输相对较为简单和直接,而路由的确定则更加复杂一些。路由算法在路由表中写入各种不同的信息,路由器会根据数据包所要到达的目的地选择最佳路径把数据包发送到可以到达该目的地的下一台路由器处。当下一台路由器接收到该数据包时,也会查看其目标地址,并使用合适的路径继续传送给后面的路由器。依次类推,直到数据包到达最终目的地。 42 | * 路由器之间可以进行相互通讯,而且可以通过传送不同类型的信息维护各自的路由表。路由更新信息主是这样一种信息,一般是由部分或全部路由表组成。通过分析其它路由器发出的路由更新信息,路由器可以掌握整个网络的拓扑结构。链路状态广播是另外一种在路由器之间传递的信息,它可以把信息发送方的链路状态及进的通知给其它路由器。 43 | 44 | ### 三层交换机 45 | * 三层交换机是带有路由功能的二层交换机,它是二者的有机结合,并不是简单的把路由器设备的硬件及软件简单地叠加在二层交换机上。 46 | * 从硬件上看,第二层交换机的接口模块都是通过高速背板/总线交换数据的;在第三层交换机中,与路由器有关的第三层路由硬件模块也插接在高速背板/总线上,这种方式使得路由模块可以与需要路由的其他模块间高速的交换数据,从而突破了传统的外接路由器接口速率的限制。 47 | * 在软件方面,第三层交换机也有重大的举措,它将传统的基于软件的路由器软件进行了界定,其做法是:对于数据包的转发,如IP/IPX包的转发,这些规律的过程通过硬件得以高速实现。其他软件功能如路由信息的更新、路由表维护、路由计算、路由的确定等,用优化、高效的软件实现。 48 | 49 | ## 地址解析协议ARP 50 | 51 | ### ARP概述 52 | 地址解析协议ARP(Address Resolution Protocol)是网络层的协议,它根据IP数据包头中的IP地址信息解析出目标MAC地址。 53 | 54 | ### ARP缓存 55 | ARP缓存是个用来储存IP地址和MAC地址的缓冲区,其本质就是一个IP地址和MAC地址的对应表,表中每一个条目分别记录了网络上其他主机的IP地址和对应的MAC地址。 56 | 57 | ### 例子 58 | 1. 主机A根据自己的路由表内容,确定主机B的IP地址,然后在本地ARP缓存中检查主机B的匹配MAC地址。 59 | 2. 如果主机A在ARP缓存中没有找到映射,就将查找主机B的IP地址的ARP请求帧广播到本地网络上的所有主机。源主机A的IP地址和MAC地址都包括在ARP请求中。 60 | 3. 本地网络上的每台主机都接收到ARP请求并且检查是否与自己的IP地址匹配。如果主机发现请求的IP地址与自己的IP地址不匹配,它将丢弃ARP请求。而主机B接收到ARP请求后确定其中的IP地址与自己的IP地址匹配,就将主机A的IP地址和MAC地址映射添加到本地ARP缓存中。 61 | 4. 主机B将包含其MAC地址的ARP回复消息直接发送回主机A。 62 | 5. 当主机A收到从主机B发来的ARP回复消息时,会用主机B的IP和MAC地址映射更新ARP缓存。本机缓存是有生存期的,生存期结束后,将再次重复上面的过程。主机B的MAC地址一旦确定,主机A就能向主机B发送IP通信了。 63 | 64 | -------------------------------------------------------------------------------- /数据库/Redis.md: -------------------------------------------------------------------------------- 1 | # Redis 2 | * Redis是一个基于内存的key-value存储系统,读取效率极高,提供多种语言的API,常被用作缓存。 3 | * Redis是单进程单线程的,利用队列技术将并发访问变为串行访问,消除了传统数据库并行控制的开销(上下文切换)。 4 | * Redis支持事务,具有事务的四大特性ACID,操作都是原子性,还可以对存入的Key-Value设置expire时间,因此也可以被当作一个功能加强版的Memcache来用。 5 | 6 | ## Redis支持的数据类型 7 | * String:字符串,这个没啥好说的。 8 | * List:有序、可重复的列表,可以做简单的消息队列。 9 | * Set:无序、不可重复的集合,提供交集、并集、差集等操作。 10 | * SortedSet:排序的不可重复的集合,基于权重参数score进行排列。 11 | * Hash:结构化的对象,可以操作其中的某个字段,类似JSON。 12 | 13 | ## Redis的数据淘汰策略 14 | * Redis采用定期删除+惰性删除策略: 15 | * 定期删除:Redis默认每隔100ms,抽取随机的数据集进行检查并删除过期的key,所以如果只采用定期删除策略,会导致很多key到时间没有删除。 16 | * 惰性删除:当获取某个key时,Redis会检查该key是否过期,如果过期了就会自动删除。 17 | * 上述两种策略存在问题:定期删除中没删除的key,也没有及时去获取它,它就会一直在内存中不会被清理,可以采用内存淘汰机制来解决这个问题,在redis.conf文件中配置。 18 | * volatile-lru:当内存不足以容纳新写入数据时,从过期数据集中挑选最近最少使用的数据淘汰。 19 | * volatile-ttl:当内存不足以容纳新写入数据时,从过期数据集中挑选将要过期的数据淘汰。 20 | * volatile-random:当内存不足以容纳新写入数据时,从过期数据集中随机选择数据淘汰。 21 | * allkeys-lru:当内存不足以容纳新写入数据时,从所有数据集中挑选最近最少使用的数据淘汰。 22 | * allkeys-random:当内存不足以容纳新写入数据时,从所有数据集中随机选择数据淘汰。 23 | * no-enviction:当内存不足以容纳新写入数据时,新写入操作会报错。 24 | 25 | ## Redis与Memcache的区别 26 | * 数据类型:Memcache的值都是简单的字符串,而Redis支持丰富的数据类型。 27 | * 数据大小:Memcache单个value的最大限制是1MB,而Redis单个value的最大限制是1GB。 28 | * 存储方式:Memcache把数据全部存在内存之中,断电后会挂掉,而且数据不能超过内存大小,而Redis则是主要数据存在内存,部分数据存在磁盘上,定期将内存数据flush进磁盘,可以实现数据的持久化存储。 29 | * 底层模型:Redis直接自己构建了虚拟内存机制,而Memcache使用一般的系统调用,这会浪费一定的时间去移动和请求,因此Redis的读写速度比Memcache快。 30 | 31 | ## 缓存穿透问题和缓存雪崩问题 32 | 33 | ### 缓存穿透问题 34 | * 缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。 35 | * 解决方案: 36 | * 利用互斥锁,缓存失效的时候,先去获得锁,得到锁再请求数据库;没得到锁则休眠一段时间重试。 37 | * 采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热,即项目启动前,先加载缓存。 38 | * 提供一个能迅速判断请求是否有效的拦截机制,比如利用布隆过滤器,内部维护一系列合法有效的key,可以迅速判断出,请求所携带的Key是否合法有效,如果不合法,则直接返回。 39 | 40 | ### 缓存雪崩问题 41 | * 缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。 42 | * 解决方案: 43 | * 给缓存的失效时间,加上一个随机值,避免集体失效。 44 | * 使用互斥锁,但是该方案吞吐量明显下降了。 45 | * 使用双缓存,假设为缓存A和缓存B,缓存A的失效时间为20分钟,缓存B不设失效时间,自己做缓存预热操作,操作细节如下: 46 | 1. 从缓存A读数据库,有则直接返回; 47 | 2. 如果A没有数据,则直接从B读数据,直接返回,并且异步启动一个更新线程; 48 | 3. 更新线程同时更新缓存A和缓存B。 49 | 50 | ## Redis分布式存储 51 | * Redis支持主-从的分布式存储模式:主数据库Master和从数据库Slave。 52 | * Master会将数据同步到Slave,而Slave不会将数据同步到Master。 53 | * Slave启动时会连接Master来同步数据。 54 | 55 | ### 读写分离模型 56 | * 利用Master来插入数据,Slave提供检索服务,这样可以有效减少单个机器的并发访问数量。 57 | * 通过增加Slave的数量,读的性能可以线性增长;为了避免Master的单点故障,集群一般都会采用两台Master做双机热备。所以整个集群的读和写的可用性都非常高。 58 | * 读写分离模型的缺陷在于,不管是Master还是Slave,每个节点都必须保存完整的数据。如果在数据量很大的情况下,集群的扩展能力还是受限于单个节点的存储能力。 59 | 60 | ### 数据分片模型 61 | * 将每个节点看成都是独立的Master,然后通过业务实现数据分片。 62 | * 为了解决读写分离模型的缺陷,可以使用数据分片模型。 63 | 64 | ## Redis备份策略 65 | Redis提供两种备份方法:RDB和AOF。 66 | 67 | ### RDB持久化 68 | * RDB是在某个时间点将内存中的所有数据的快照保存到磁盘上,在数据恢复时,可以恢复备份时间以前的所有数据,但无法恢复备份时间点后面的数据。 69 | * 默认情况下Redis在磁盘上创建二进制格式的命名为dump.rdb的数据快照,可以通过配置文件配置每隔N秒且数据集上至少有M个变化时创建快照,还可以配置是否对数据进行压缩、快照名称、存放快照的工作目录。 70 | 71 | ### AOF持久化 72 | * AOF是以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到 AOF 文件,以此达到记录数据库状态的目的。 73 | * 优点是基本可以实现数据无丢失(缓存的数据有可能丢失),缺点是随着数据量的持续增加,AOF文件也会越来越大。 74 | -------------------------------------------------------------------------------- /数据库/MySQL.md: -------------------------------------------------------------------------------- 1 | # MySQL 2 | 一种开源的关系型数据库,使用SQL进行数据库管理。 3 | 4 | ## MySQL的存储引擎:MyISAM和InnoDB 5 | MyISAM由早期的ISAM(Indexed Sequential Access Method,有索引的顺序访问方法)改良,虽然性能极佳,但它有一个显著的缺点:不支持事务处理,因此MySQL引入了另一种引擎:InnoDB,它可以支持ACID兼容的事务功能,可以强化参考完整性与并发违规处理机制,后来就逐渐取代MyISAM。 6 | 7 | ### 存储结构 8 | * MyISAM:每个表在磁盘上存储成三个文件,分别存储表定义、数据文件和索引文件。 9 | * InnoDB:所有的表都保存在同一个数据文件中,也可能是多个文件,或者是独立的表空间文件,InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。 10 | 11 | ### 存储空间 12 | * MyISAM:可被压缩,占据的存储空间较小,支持静态表、动态表、压缩表三种不同的存储格式。 13 | * InnoDB:需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。 14 | 15 | ### 可移植性:备份及恢复 16 | * MyISAM:数据以文件的形式存储,所以数据转移很方便,在备份和恢复时也可单独对某个表进行操作。 17 | * InnoDB:用`mysqldump`命令备份,在数据量大时相对麻烦。 18 | 19 | ### 事务支持 20 | * MyISAM:强调性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。 21 | * InnoDB:提供事务、外键等高级数据库功能,具有事务提交、回滚和崩溃修复能力,支持四种隔离级别。 22 | 23 | ### 锁机制 24 | * MyISAM:只支持表级锁,用户在操作MyISAM表时,`SELECT`、`UPDATE`、`DELETE`和`INSERT`语句都会给表自动加锁。 25 | * 表共享读锁:不会阻塞其他用户对同一表的读操作,但会阻塞写操作。 26 | * 表独占写锁:会阻塞其他用户对同一表的读和写操作。 27 | * InnoDB:支持事务和行级锁,可以大幅提高并发性能,但是InnoDB的行锁只对WHERE子句中的主键有效,非主键的WHERE会锁全表。 28 | * 行共享锁:允许一个事务去读一行,阻止其他事务获得相同数据集的行排他锁。用`SELECT ... IN SHARE MODE`可以获取行共享锁。 29 | * 行排他锁:允许一个事务更新数据,阻止其他事务获得相同数据集的行共享锁和行排他锁。用`SELECT ... FOR UPDATE`可以获取行排他锁。 30 | 31 | ### 索引和主键 32 | * MyISAM:允许没有任何索引和主键的表存在,所有索引都是非聚集索引。 33 | * InnoDB:如果没有设定主键或者非空唯一索引,就会自动生成一个用户不可见的主键,主索引是聚集索引,其他索引是非聚集索引。 34 | 35 | ### 外键 36 | MyISAM不支持外键,而InnoDB支持外键。 37 | 38 | ## InnoDB的间隙锁(Next-Key锁) 39 | * 概念:当检索范围数据并请求共享或排他锁时,InnoDB会给符合条件的数据的索引项加锁,间隙是指键值在条件范围内但并不存在的记录,InnoDB也会对这些记录加锁。 40 | * 举例:假如item表中有10条记录,其id的值分别是1到10,查询语句`SELECT * FROM item WHERE id > 9 FOR UPDATE`是一个范围条件的检索,InnoDB不仅会对符合条件的id值为10的记录加锁,也会对id大于10(这些记录并不存在)的“间隙”加锁。 41 | * 注意事项:InnoDB使用间隙锁的目的是防止幻读,以满足相关隔离级别的要求,但会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。 42 | 43 | ## MySQL中char、varchar和text的区别 44 | 45 | ### char(n) 46 | * 存储定长字符串,可以指定默认值,索引效率非常高,必须在括号里定义长度n,超过会被截断,n的最大值是255。 47 | * n也是存储空间的实际长度,不管实际字符串有多长,都会占用n个字符的空间,后面会用空格填充,如果原数据末尾就存在空格,检索取出时会丢失。 48 | 49 | ### varchar(n) 50 | * 存储变长字符串,可以指定默认值,存储效率没有char(n)高,可以在括号里定义长度n,超过会被截断,所以实际可以保存的最大字符串长度是n-1,因为在字符串被截断时,会留一个空间保存字符串结束的标记。 51 | * 保存数据的时候不进行空格自动填充,而且如果数据存在空格,保存和检索时尾部的空格仍会保留。 52 | * varchar(n)可以指定的长度上限是65535字节,但实际情况会有1-3个字节存储字符串的长度信息,所以实际可以保存的字符串的最大长度为65532字节。 53 | 54 | ### text 55 | * text是用来存储较长字符串的结构,其存储原理与varchar(n)相同,效率也相似。 56 | * text与varchar(n)的区别主要是:text不能限定最大长度n,也不能指定默认值,而且使用外部存储来存储字符长度,不占用字符串空间,最大支持字符串长度为65535个字节。 57 | 58 | ## 慢查询日志 59 | * 用来记录响应时间超过阀值的查询操作,可以用来找出有问题的SQL语句。 60 | * 相关参数: 61 | * `slow_query_log`:是否开启慢查询日志。 62 | * `long_query_time`:慢查询阀值,当查询时间多于设定的阀值时,记录日志。 63 | * `log_queries_not_using_indexes`:未使用索引的查询也被记录到慢查询日志中。 64 | * `log_output`:日志存储方式,可以文件形式存储,也可以存入数据库,还可以两者同时启用。 65 | 66 | ## explain命令 67 | * 用于查看执行查询效果,展示了MySQL如何使用索引来处理select语句以及连接表,可以帮助选择更好的索引和写出更优化的查询语句。 68 | * 使用方法:在select语句前加上explain,如explain select * from user;。 69 | * 结果中每一列的解释: 70 | * id:表示select查询序列号,代表SQL语句执行的顺序。 71 | * select_type:表示查询类型,包括simple、primary、union、dependent union和union result。 72 | * table:表示该行数据是关于哪张表的。 73 | * type:表示连接使用了什么类型,从好到坏依次为const、eq_reg、ref、range、index和ALL。 74 | * possible_keys:表示可能应用在这张表中的索引,如果为空则表示没有可用的索引。 75 | * key:表示查询中实际使用的索引,如果为空则表示没有使用索引。 76 | * key_len:表示使用的索引的长度,在不损失精确性的情况下,长度越短越好。 77 | * ref:表示索引的哪一列被使用了,如果可能的话是一个常数。 78 | * rows:表示返回查询结果必须检查的数据行数,数值越大越不好,说明没有用好索引。 79 | * extra:表示MySQL如何解析查询的额外信息。 -------------------------------------------------------------------------------- /操作系统/进程管理.md: -------------------------------------------------------------------------------- 1 | # 进程管理 2 | 3 | ## 进程 4 | 5 | ### 进程的概念 6 | 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统进行资源分配和调度的独立单位。 7 | 8 | ### 进程的状态与转换 9 | * 三态模型 10 | ![进程的三态模型](media/进程管理/进程的三态模型.jpg) 11 | * 五态模型 12 | ![进程的五态模型](media/进程管理/进程的五态模型.jpg) 13 | 14 | ### 进程的控制 15 | * 进程控制块(Process Control Block,PCB):是一个OS用于记录和刻画进程状态及环境信息的数据结构,包括三个部分:标示信息、现场信息和控制信息。 16 | * 进程映像(Process Image,PI):是某一时刻进程的内容及其执行状态集合,是内存级的物理实体,包括四个部分:进程控制块、进程程序块、进程数据块和核心栈。 17 | * 进程上下文(Process Context,PC):是进程执行需要的环境支持,由OS中的进程物理实体和支持进程运行的环境组成,包括三个部分:用户级上下文、寄存器上下文和系统级上下文。 18 | 19 | ### 进程的组织(进程队列模型) 20 | ![进程队列模型](media/进程管理/进程队列模型.jpg) 21 | 22 | ### 进程间通信方式 23 | * 消息队列(MessageQueue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。 24 | * 共享内存(SharedMemory):共享内存就是多个进程使用内存来存取数据,是最快的进程间通信方式方式,往往与其他同步机制配合使用,比如信号量。 25 | * 管道(Pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在父子进程间使用。 26 | * 套接字(Socket):套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。 27 | * 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 28 | 29 | ## 线程 30 | 31 | ### 线程概念与多线程模型 32 | * 线程是进程的一条执行路径,是调度的基本单位,同一个进程中的所有线程共享进程获得的主存空间和资源。 33 | * 进程是执行着的应用程序,而线程是进程内部的一个执行序列,一个进程可以有多个线程。 34 | 35 | ### 线程的上下文切换及其开销 36 | * 线程的上下文切换是指:当一条线程的时间片用完后,操作系统会暂停该线程,并保存该线程相应的信息,然后再选择另一条线程去执行。 37 | * 每次进行上下文切换时都需要保存当前线程的执行状态,并加载新线程先前保存的状态。所以如果上下文切换频繁,CPU花在上下文切换上的时间占比就会上升,而真正处理任务的时间占比就会下降。因此,为了提高并发程序的执行效率,让CPU把时间花在刀刃上,我们需要减少上下文切换的次数。 38 | 39 | ### 如何减少上下文切换 40 | * 减少线程的数量:最简单的做法,只要减少线程的数量,就能减少上下文切换的次数。然而如果线程数量已经少于CPU核数,每个CPU执行一条线程,照理来说CPU已经不需要上下文切换了,但事实并非如此。 41 | * 减少同一把锁上的线程数量:如果多条线程共用同一把锁,那么当一条线程获得锁后,其他线程就会被阻塞,就会出现上下文切换;当该线程释放锁后,操作系统会从被阻塞的线程中选一条执行,从而又会出现上下文切换。因此,减少同一把锁上的线程数量也能减少上下文切换的次数。 42 | * 采用无锁并发编程 43 | * Hash分段(需要并发执行的任务是无状态的):所谓无状态是指并发执行的任务没有共享变量,他们都独立执行。对于这种类型的任务可以按照ID进行Hash分段,每段用一条线程去执行。 44 | * CAS算法(需要并发执行的任务是有状态的):如果任务需要修改共享变量,那么必须要控制线程的执行顺序,否则会出现安全性问题。你可以给任务加锁,保证任务的原子性与可见性,但这会引起阻塞,从而发生上下文切换。为了避免上下文切换,你可以使用CAS算法,仅在线程内部需要更新共享变量时使用CAS算法来更新,这种方式不会阻塞线程,并保证更新过程的安全性。 45 | 46 | ### 线程间通信方式 47 | * 共享内存 48 | * 共享内存是指:多条线程共享同一片内存,发送者将消息写入内存,接收者从内存中读取消息,从而实现了消息的传递。 49 | * 但这种方式有个弊端,即需要程序员来控制线程的同步,即线程的执行次序。 50 | * 这种方式并没有真正地实现消息传递,只是从结果上来看就像是将消息从一条线程传递到了另一条线程。 51 | * 消息队列 52 | * 顾名思义,消息传递指的是发送线程直接将消息传递给接收线程。 53 | * 由于执行次序由并发机制完成,因此不需要程序员添加额外的同步机制,但需要声明消息发送和接收的代码。 54 | 55 | ### 线程池 56 | * 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。 57 | * 我们知道线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况。运用线程池能有效的控制线程最大并发数,避免以上的问题。 58 | * 线程池是指:提供一个容器,使得线程可以复用,就是执行完一个任务后不会被销毁,而是可以继续执行其他任务。 59 | 60 | ## 处理机调度 61 | 62 | ### 调度的基本概念 63 | * 高级调度:高级调度又称为作业调度或长程调度,其主要功能是根据某种算法,把外存上处于后备队列中的那些作业调入内存,也就是说,它的调度对象是作业。 64 | * 中级调度:中级调度又称中程调度。引入中程调度的主要目的是为了提高内存利用率和系统吞吐量。中级调度实际上就是存储器管理中的对换功能。 65 | * 低级调度:低级调度通常也称为进程调度或短程调度,它所调度的对象是进程(或内核级线程),进程调度是最基本的一种调度,在多道批处理、分时、实时三种类型的OS中,都必须配置这级调度。 66 | 67 | ### 典型调度算法 68 | * 先来先服务调度算法 69 | * 短作业(短任务、短进程、短线程)优先调度算法 70 | * 时间片轮转调度算法 71 | * 高响应比优先调度算法 72 | * 多级反馈队列调度算法 73 | 74 | ## 进程同步 75 | 76 | ### 基本概念 77 | * 进程同步:并发进程之间为完成共同任务基于某个条件来协调执行先后关系而产生的协作制约关系。 78 | * 进程互斥:并发进程之间因相互争夺独占性资源而产生的竞争制约关系。 79 | * 临界资源:互斥共享变量所代表的资源,即一次只能被一个进程使用的资源。 80 | * 临界区:并发进程中与互斥共享变量相关的程序段。 81 | 82 | ### 实现临界区互斥的基本方法 83 | * 在进出临界区时关中断,这样临界区执行就不会中断了,执行就有原子性。 84 | * 关中断 -> 临界区 -> 开中断 85 | * 操作系统原语就采用这种实现思路。 86 | 87 | ### 软件和硬件实现方法 88 | ### 信号量和PV操作 89 | ### Hoare管程 90 | 91 | ## 死锁 92 | 93 | ### 死锁的概念 94 | * 对于一组进程,每个进程都在等待其他进程执行完毕才能继续往下执行的时候就发生了死锁,结果就是这组进程全部陷入无限的等待中。 95 | * 例如:进程A要先获取锁A再获取锁B,—进程B要先获取锁B再获取锁A。在执行过程中有可能出现:进程A获取了锁A,请求锁B;进程B获取了锁B,请求锁A。这就出现了死锁,进程A和B都无法继续执行。 96 | 97 | ### 死锁的避免 98 | * 加锁顺序:合理指定线程获取锁的顺序,并强制线程按照指定的顺序获取锁。 99 | * 加锁时限:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁。 100 | * 死锁检测: 101 | * 每当一个线程获得了锁,会在线程和锁相关的数据结构中将其记下。每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。 102 | * 检测出死锁时,一个可行的做法是释放所有线程的锁,各自等待一段随机时间后重试;一个更好的方案是给这些线程设置优先级,让一个或几个线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。 103 | 104 | -------------------------------------------------------------------------------- /数据结构/图(Graph).md: -------------------------------------------------------------------------------- 1 | # 图(Graph) 2 | 3 | ## 简介 4 | 图是G =(V,E)的有序对,其包括顶点或节点的集合V以及边或弧的集合E,其中E包括了两个来自V的元素(即边与两个顶点相关联,并且该关联为这两个顶点的无序对)。 5 | 6 | ## 无向图和有向图 7 | * 无向图:图的邻接矩阵是对称的,因此如果存在节点u到节点v的边,那节点v到节点u的边也一定存在。 8 | * 有向图:图的邻接矩阵不是对称的。因此如果存在节点u到节点v的边并不意味着一定存在节点v到节点u的边。 9 | 10 | ## 图的存储结构 11 | 12 | ### 邻接矩阵 13 | * 邻接矩阵是表示顶点之间相邻关系的矩阵,适于存储边的数目较多的稠密图。 14 | * 假设图中顶点数为n,则邻接矩阵An×n: 15 | * 无权图:对于若Vi和Vj之间有边,A[i][j]=1;反之,A[i][j]=0。 16 | * 带权图:对于若Vi和Vj之间有边,A[i][j]=该边的权值;反之,A[i][j]=0。 17 | * 注意: 18 | * 图中无邻接到自身的弧,因此邻接矩阵主对角线为全零。 19 | * 无向图的邻接矩阵一定是一个对称矩阵,可采用压缩存储的思想,只存储上(下)三角形阵的元素即可。 20 | * 局限性:要确定图中有多少条边,必须按行、列检测每个元素,花费时间代价很大。 21 | * 示例: 22 | ![邻接矩阵](media/图论/邻接矩阵.jpg) 23 | 24 | ### 邻接表 25 | * 基本思想:为图中的每个顶点Vi建立一个单链表,链表中结点表示依附于该顶点的边。 26 | * 注意: 27 | * 在无向图的邻接表中,第i个链表中结点数目为顶点i的度;所有链表中结点数目的一半为图中边数;占用的存储单元数目为n+2e。 28 | * 在有向图的邻接表中,第i个链表中结点数目为顶点i的出度;所有链表中结点数目为图中弧数;占用的存储单元数目为n+e。 29 | * 为求出每一个顶点的入度,必须另外建立有向图的逆邻接表。有向图的逆邻接表与邻接表类似,只是它是从入度考虑结点,而不是从出度考虑结点。 30 | * 示例: 31 | ![邻接表](media/图论/邻接表.png) 32 | 33 | ## DFS和BFS 34 | 注:如果图中有环,可能会造成死循环。解决办法是:遍历时记录哪些点已被遍历过,哪些点没被遍历到,通常使用Hash实现。 35 | 36 | ### 深度优先搜索(Depth First Search,DFS) 37 | * 深度优先搜索是一种先遍历子节点而不回溯的遍历算法。 38 | * 使用递归或栈Stack实现。 39 | * 时间复杂度:O(|V| + |E|) 40 | 41 | ### 广度优先搜索(Breadth First Search,BFS) 42 | * 广度优先搜索是一种先遍历邻居节点而不是子节点的遍历算法。 43 | * 使用队列Queue实现。 44 | * 时间复杂度:O(|V| + |E|) 45 | 46 | ## 单源最短路径 47 | * 单源最短路径,在现实中是很多应用的,是图的经典应用,比如在地图中找出两个点之间的最短距离、最小运费等。单源最短路径的问题:已知图G=(V,E),找出给定源顶点s∈V到每个顶点v∈V的最短路径。 48 | * 衍生出的变体问题如下: 49 | * 单终点最短路径问题:找出从每个顶点v到指定终点t的最短路径。这个是单源最短路径的反向,把图的每条边反向,问题就变成单源最短路径的问题; 50 | * 单对顶点间最短路径问题:对于给定顶点u和v,找出从u到v的一条最短路径。找出所有顶点的单源最短路径,该问题自然得解。 51 | * 每对顶点间最短路径问题:对于任意给定顶点u和v,找出从u到v的最短路径。 52 | 53 | ### Dijkstra算法 54 | * Dijkstra算法是一种查找单源最短路径的算法,要求每条边的权值非负。 55 | * 算法流程: 56 | 1. 首先需要记录每个点到原点的最短距离Di,这个距离会在每一轮遍历的过程中刷新。Di的初始状态为:若起始点到Vi存在边,则Di=该边的权值;否则Di=∞。 57 | 2. 从Di中选取一个最短的边,遍历由该边连接的点的所有边,更新最短距离Di。 58 | 3. 重复第二步,经过n-1轮计算就能得到从起始点到其他每个节点的最短距离。 59 | * 时间复杂度:O(|V|^2 ) 60 | * 示例: 61 | ![Dijkstra算法示例](media/图论/Dijkstra算法示例.gif) 62 | 63 | ### Bellman-Ford算法 64 | * Bellman-Ford是一种在带权图中查找单一源点到其他节点最短路径的算法。虽然时间复杂度大于Dijkstra算法,但它可以处理包含了负值边的图。 65 | * 时间复杂度: 66 | * 最优:O(|E|) 67 | * 最差:O(|V|*|E|) 68 | 69 | ### Floyd-Warshall算法 70 | * Floyd-Warshall算法是一种在无环带权图中寻找任意节点间最短路径的算法。该算法执行一次即可找到所有节点间的最短路径(路径权重和)。 71 | * 算法流程: 72 | 1. 引入两个矩阵,矩阵D中的元素D[i][j]表示顶点i到顶点j的距离。矩阵P中的元素P[i][j],表示顶点i到顶点j经过了P[i][j]记录的值所表示的顶点。 73 | 2. 假设图G中顶点个数为N,则需要对矩阵D和矩阵P进行N次更新。初始时,矩阵D中顶点D[i][j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则D[i][j]=∞,矩阵P的值为顶点P[i][j]中j的值。 74 | 3. 接下来开始,对矩阵D进行N次更新:如果D[i][j] > (D[i][k]+D[k][j])(表示i与j之间经过第k个顶点的距离),则更新D[i][j] = (D[i][k]+a[k][j]),更新P[i][j] = P[i][k]。其中k=1..N(或0..N-1),即循环N次。 75 | * 时间复杂度: 76 | * 最优:O(|V|^3 ) 77 | * 最差:O(|V|^3 ) 78 | * 平均:O(|V|^3 ) 79 | 80 | ## 最小代价生成树 81 | 82 | ### Prim算法 83 | * Prim算法是一种在无向带权图中查找最小生成树的贪心算法,能够在一个图中找到连接所有节点的边的最小子集。 84 | * 算法流程:从图中任意取出一个顶点,将其当作一棵树,然后从这棵树相邻的边中选取一条最短(权值最小)的边,并将这条边连接的顶点也并入这棵树中,不断重复这个过程,直到所有顶点都并入这棵树中,此时得到的树就是最小生成树。 85 | * 时间复杂度:O(|V|^2) 86 | 87 | ### Kruskal算法 88 | * Kruskal算法也是一个计算最小生成树的贪心算法,但在Kruskal算法中,图不一定是连通的。 89 | * 算法流程: 90 | 1. 将图中的所有边都去掉。 91 | 2. 将边按权值从小到大的顺序添加到图中,并且保证添加的过程中不会形成环。 92 | 3. 重复上一步,直到连接所有顶点,此时得到的树就是最小生成树。 93 | * 时间复杂度:O(|E|log|V|) 94 | 95 | ## 拓扑排序 96 | * 对一个有向无环图(Directed Acyclic Graph,DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若存在边(u,v)∈E(G),则u在线性序列中出现在v之前。 97 | * 拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行。为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex Network),简称AOV网。 98 | * 一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行(对于数据流来说就是死循环)。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological Order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。 99 | * 实现步骤 100 | 1. 在有向图中选一个没有前驱的顶点并且输出; 101 | 2. 从图中删除该顶点和所有以它为起始点的边; 102 | 3. 重复上述两步,直到所有顶点都被输出,或者当前图中不存在无前驱的顶点为止。后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。 103 | 104 | ## 常见应用 105 | 106 | -------------------------------------------------------------------------------- /计算机网络/计算机网络参考模型.md: -------------------------------------------------------------------------------- 1 | # 计算机网络参考模型 2 | 3 | ## OSI七层模型 4 | | 层次 | 功能 | 5 | | --- | --- | 6 | | 物理层 | 在设备间传送比特数据,定义介质的规范。 | 7 | | 数据链路层 | 为在局域网上主机到主机或广域网的设备间发送建立和编制帧,为物理层提供可靠的数据传输,主要关心三个基本问题:封装成帧、透明传输和差错检验。 | 8 | | 网络层 | 为两个终端系统提供连接和路径选择。 | 9 | | 传输层 | 在源定义数据段并编号,传送数据,并在目的地重组数据,负责终端节点之间的可靠网络通信,提供建立、维护和结束虚电路的机制、差错检验和恢复、流控制的服务。 | 10 | | 会话层 | 在用户间建立和管理会话,同步展示层间的对话并处理数据交换,提供有效的数据传输、服务等级和会话层、展示层、应用层的异常报告。 | 11 | | 展示层 | 提供数据的编码、压缩和加密功能。 | 12 | | 应用层 | 最接近用户的层,为用户的应用程序提供网络服务。 | 13 | 14 | ## 数据封装(Encapsulation) 15 | 16 | ### 数据链路层:以太网帧(Frame) 17 | ![以太网帧](media/计算机网络参考模型/以太网帧.jpg) 18 | 19 | * 其中的源地址和目的地址是指网卡的硬件地址(也叫MAC地址),长度是48位(6个字节),是在网卡出厂时固化的。 20 | * 帧末尾是CRC校验码。 21 | * 注意网卡芯片(例如DM9000A)收到的数据就是如上所示的一长串数据;其中包括以太网帧头、IP报报头、传输层协议段头、应用层所需数据。 22 | * 以太网帧中的数据长度规定最小46字节,最大1500字节。ARP和RARP数据包的长度不够46字节,要在后面补填充位(PAD)。 23 | * 最大值1500称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包度大于拨号链路的MTU了,则需要对数据包进行分片(Fragmentation)。注意,MTU指数据帧中有效载荷的最大长度,不包括帧首部的长度。 24 | 25 | ### 网络层:IP数据报(Datagram) 26 | ![IP数据报](media/计算机网络参考模型/IP数据报.png) 27 | 28 | * 首先注意,这里说的是IPv4。 29 | * 4位版本号指定IP协议的版本。对IPv4来说,其值是4。 30 | * 4位头部长度是指该IP数据报头部有多少个32bit(4字节),最⼩值为5。也就是说⾸部长度最⼩是4x5=20字节,也就是不带任何选项的IP⾸部;4位能表⽰的最⼤值是15,也就是说⾸部长度最⼤是60字节。 31 | * 8位TOS字段有3个位⽤来指定IP数据报的优先级(⽬前已经废弃不⽤),还有4个位表⽰可选的服务类型(最⼩延迟、最⼤呑吐量、最⼤可靠性、最⼩成本),剩下的1个位总是0。 32 | * 16位总长度是整个数据报(包括⾸部和数据)的字节数,以字节为单位,因此IP数据报的最大长度为65535(2^16-1 ) 字节。但由于MTU的限制,长度超过MTU的数据报都将被分片传输,所以实际传输的IP数据报(或分片)的长度都远远没有达到最大值。 33 | * 16位标识唯一地标识主机发送的每一个数据报。初始值随机生成,每发送一个数据报,其值就加1。该值在数据报分片时被复制到每个分片中,因此同一个数据报的所有分片都具有相同的标识值。 34 | * 3位分片标志字段的第1位保留;第2位表示“禁止分片”,即如果设置了这个位,IP将不对数据报进行分片。在这种情况下,如果IP数据报长度超过MTU的话,将丢弃该数据报并返回一个ICMP差错报文;第3位表示“还有更多分片”,即除了数据报的最后一个分片外,其他分片都要把它置1。 35 | * 13位分片偏移是分片相对原始IP数据报开始处理(仅指数据部分)的偏移。实际的偏移值是该值左移3位(乘8)后得到的。由于这个原因,除了最后一个IP分片外,每个IP分片的数据部分的长度必须是8的整数倍(这样才能保证后面的IP分片拥有一个合适的偏移)。 36 | * 8位生存时间(Time To Live,TTL)是数据报到达目的地之前允许经过的路由器跳数。TTL值被发送端设置(常见的值为64)。数据报在转发过程中每经过一个路由,该值就被路由器减 1。当TTL值减为0时,路由器将丢弃数据报,并向源端发送一个ICMP差错报文。TTL值可以防止IP数据报陷入路由循环。 37 | * 8位协议用来区分上层协议,/etc/prmocols文件定义了所有上层协议对应的数值。其中ICMP是1,TCP是6,UDP是17。/etc/protocols文件是RFC 1700的一个子集。 38 | * 16位头部校验和由发送端填充,接收端对其使用CRC算法以检验IP数据报头部(注意,仅检验头部)在传输过程中是否损坏。 39 | * 32位的源IP地址和目的IP地址用来标识数据报的发送端和接收端。 40 | * 选项字段是可变长的可选信息。这部分最多包含40字节,因为IP头部最长60字节(其中还包含前面讨论的20字节的同定部分)。可用的IP选项包括:记录路由、时间戳、松散路由源路由选择、严格源路由选择。 41 | * 选项后面接的就是数据段。 42 | 43 | ### 传输层:UDP段(Segment) 44 | ![UDP段](media/计算机网络参考模型/UDP段.png) 45 | 46 | ### 传输层:TCP段(Segment) 47 | ![TCP段](media/计算机网络参考模型/TCP段.png) 48 | 49 | * 源端口(16位)和目的端口(16位):标识发送和接收报文的计算机端口或进程。一个TCP报文段必须包括源端口号,使目的主机知道应该向何处发送确认报文。 50 | * 序号(32位):用于标识每个报文段,使目的主机可确认已收到指定报文段中的数据。当源主机用于多个报文段发送一个报文时,即使这些报文到达目的主机的顺序不一样。在SYN标志未置位时,该字段指示了用户数据区中第一个字节的序号;在SYN标志已置位时,该字段指示的是初始发送的序列号。在建立连接时发送的第一个报文段中,双方都提供一个初始序列号。TCP标准推荐使用以4ms间隔递增1的计数器值作为这个初始序列号的值。使用计数器可以防止连接关闭再重新连接时出现相同的序列号。对于那些包含数据的报文段,报文段中第一个数据字节的数量就是初始序列号,其后数据字节按顺序编号。如果源主机使用同样的连接发送另一个报文段,那么这个报文段的序列号等于前一个报文段的序列号与前一个报文段中数据字节的数量之和。如果序列号增大至最大值将复位为0。 51 | * 确认号(32位):目的主机返回确认号,使源主机知道某个或几个报文段已被接收。例如,序列号等于前一个报文段的序列号与前一个报文段中数据字节的数量之和,比如说假设源主机发送3个报文段,每个报文段有100字节的数据,且第一个报文段的序列号是1000,那么接收到第一个报文段后,目的主机返回含确认号1100的报头。接收到第二个报文段(其序号为1100)后,目的主机返回确认号1200。接收到第三个报文段后,目的主机返回确认号1300。但是目的主机不一定在每次接收到报文段后都返回确认号,比如说在上面的例子中,目的主机可能等到所有3个报文段都收到后,再返回一个含确认号1300的报文段,表示已接收到全部数据。但是如果目的主机再发回确认号之前等待时间过长,源主机会认为数据没有到达目的主机,并自动重发。上面的例子中,如果目的主机接收到了报文段号为1000的第一个报文段以及报文段号为1200的最后一个报文段,则可返回确认号1100,但是在返回确认号1300之前,应该等待报文段号为 1100 的中间报文段。 52 | * 数据偏移(4位):指TCP段首部的长度。 53 | * 保留位(6位):由跟在数据偏移字段后的6位构成,全部为0。 54 | * 控制位(6位) 55 | * SYN:表示建立连接; 56 | * FIN:表示关闭连接; 57 | * ACK:表示响应; 58 | * PSH:表示有DATA数据传输; 59 | * RST:表示连接重置; 60 | * UGR:表示紧急。 61 | * 窗口(16位):本机期望一次接收的字节数,即本机接收窗口大小。告诉对方在不等待确认的情况下,可以发来多大的数据。这里表示的最大长度是2^16 - 1 = 65535,如需要使用更大的窗口大小,需要使用选项中的窗口扩大因子选项。、 62 | * 校验和(16位):源主机和目的主机根据TCP报文段以及伪报头的内容计算校验和。在伪报头中存放着来自IP报头以及TCP报文段长度信息。与UDP一样,伪报头并不在网络中传输,并且在校验和中包含伪报头的目的是为了防止目的主机错误地接收存在路由的错误数据报。 63 | * 紧急指针(16位):仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据在普通数据前面)。注意:即使窗口为零时也可发送紧急数据。例如,如果报文段的序号是1000,前8个字节都是紧急数据,那么紧急指针就是8。紧急指针一般用来使用户可以中止进程。 64 | * 选项、填充字段:可能包括窗口扩大因子、时间戳等选项。长度可变,最长可达40字节,当没有使用选项时,TCP首部长度是20字节。填充用于保证选项字段的长度为32bit的整数倍。 65 | * 数据(长度可变):TCP段中的数据。 66 | 67 | ## 五层模型和TCP/IP四层模型 68 | ![不同模型对比](media/计算机网络参考模型/不同模型对比.jpg) 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /计算机网络/OSI第四层:传输层(Transport).md: -------------------------------------------------------------------------------- 1 | # OSI第四层:传输层(Transport) 2 | 3 | ## 传输层的功能 4 | * 将上层应用的数据分段。 5 | * 建立端到端的连接并将数据段从一台主机发送到其他主机。 6 | * 流控制(滑动窗口机制)和可靠性保证(序号和确认机制)。 7 | 8 | ## TCP与UDP的区别 9 | | TCP | UDP | 10 | | --- | --- | 11 | | 传输控制协议,Transmission Control Protocol | 用户数据报协议,User Datagram Protocol | 12 | | 保证可靠的面向连接协议 | 不保证可靠的无连接协议 | 13 | | 有确认响应 | 无确认响应 | 14 | | 提供流控制:滑动窗口机制和拥堵避免机制 | 不提供流控制 | 15 | | 开销大 | 开销小,高效 | 16 | | 不支持多播和广播 | 支持多播和广播 | 17 | | 传送的数据单位是TCP报文段 | 传送的数据单位是UDP数据报 | 18 | 19 | ## TCP连接的建立与释放 20 | * 三次握手建立连接:![TCP三次握手建立连接](media/OSI第四层:传输层/TCP三次握手建立连接.jpg) 21 | * 为什么是三次握手建立连接? 22 | * 一次握手:TCP是面向连接的,一次握手肯定建立不了连接,一条信息发出去连个回信都没有怎么连接? 23 | * 两次握手:当A想要建立连接时发送一个SYN,然后等待ACK,结果这个SYN因为网络问题没有及时到达B。所以A在一段时间内没收到ACK后,再发送一个SYN,这次B成功收到了,然后A也收到ACK。这时A发送的第一个SYN终于到了B,对于B来说这是一个新连接请求,然后B又为这个连接申请资源,返回ACK,然而这个SYN是个无效的请求,A收到这个SYN的ACK后也并不会理会它,但B不知道,B会一直为这个连接维持着资源,造成资源的浪费。服务器的资源是很宝贵的,不能浪费啊。 24 | * 四次或以上握手:三次握手已经是能够使双方知道对方设备都是好用的最少次数,四次或以上握手会浪费资源。 25 | * 四次握手释放链接:![TCP四次握手释放链接](media/OSI第四层:传输层/四次握手释放链接.jpg) 26 | * 为什么是四次握手释放链接? 27 | 1. 当主机A确认发送完数据且知道B已经接受完了,想要关闭TCP通道,就会发FIN给主机B。 28 | 2. 主机B收到A发送的FIN,表示收到了,就会发送ACK回复。 29 | 3. 但这是B可能还在发送数据,没有想要关闭数据口的意思,所以FIN与ACK不是同时发送的,而是等到B数据发送完了,才会发送FIN给主机A。 30 | 4. A收到B发来的FIN,知道B的数据也发送完了,就回复ACK表示收到。A等待2MSL以后,没有收到B传来的任何消息,知道B已经收到自己的ACK后关闭连接了,A就也关闭连接。 31 | * 为什么要等待2MSL? 32 | * 2MSL是指2倍的MSL。 33 | * MSL:Maximum Segment Lifetime,是指任何报文在网络上存在的最长时间,超过这个时间的话报文将被丢弃。 34 | * 等待2MSL时间主要目的是怕最后一个ACK对方没收到,那么对方在超时后将重发第三次握手的FIN,那自己接到重发的FIN包后就要再发一次第四次握手的ACK,确保对方关闭。 35 | 36 | ## TCP如何保证数据的可靠传输 37 | 38 | ### 滑动窗口机制 39 | * 原理 40 | 1. 客户端和服务器分别设定自己的窗口大小,在建立TCP连接的时候双方交换信息后,双方就知道了彼此的窗口大小。 41 | 2. 比如主机A的发送窗口大小为5,主机A可以向主机B发送5个单元,如果B缓冲区满了,A就要等待B确认才能继续发送数据。而如果B缓冲区中有1个报文被进程读取,主机B就会回复ACK给主机A,接收窗口向前滑动。 42 | 3. 只有接收窗口向前滑动并发送了确认时,发送窗口才能向前滑动。 43 | 4. 有利于控制流量,避免网络拥堵。 44 | * 示例图:![滑动窗口机制](media/OSI第四层:传输层/滑动窗口机制.jpg) 45 | 46 | ### 超时重传机制 47 | * TCP协议要求在发送端每发送一个报文段,就启动一个定时器并等待确认信息。如果接收端成功接收新数据后返回确认信息则发送下一个报文段;如果在定时器超时前数据未能被确认,就认为报文段中的数据已丢失或损坏,需要重传报文段中的数据,直到发送成功为止。 48 | * 影响超时重传机制协议效率的一个关键参数是超时重传时间(RTO,Retransmission Time Out),该值设置得过大或过小都会对协议造成不利影响: 49 | * RTO设长了,重发得太慢,没有效率,性能差。 50 | * RTO设短了,重发得太快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。 51 | * TCP协议使用自适应算法(Adaptive Retransmission Algorithm)以适应互联网分组传输时延的变化。这种算法的基本要点是TCP监视每个连接的性能(即传输时延),由此推算出合适的RTO值。当连接时延性能变化时,TCP也能够相应地自动修改RTO的设定,以适应这种网络的变化。 52 | 53 | ### 停止等待协议 54 | * 停止等待协议就是每发送完一组数据后,等收到对方发回的确认后,再发送下一组数据。 55 | * 停止等待协议会出现以下4种情况: 56 | * 无差错:正常情况,继续发送下一组数据。 57 | * 数据丢失:接收方没有收到数据分组,那么接收方不会发出确认,发送方过一段时间没有收到确认,就认为刚才的分组丢了,然后就会再次发送。 58 | * 确认丢失:发送方发送成功,接收方接收成功,确认被发送但是丢失了,那么到了等待时间,发送方没有收到确认,又会发送数据过去,此时接收方前面已经收到了数据,那么此时接收方会丢弃刚收到的数据,重新发送一次确认。 59 | * 确认迟到:发送方发送成功,接收方接收成功,确认被发送也没有丢失,但是由于传输太慢,等到了发送方设置的时间,发送方又会重新发送数据,此时接收方会丢弃收到的数据,重新发送确认。发送方如果收到两个或者多个确认,就停止发送,丢弃其他确认。 60 | 61 | ### 拥塞控制机制 62 | 拥塞控制是指,防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不会过载。 63 | 64 | #### 慢开始(Slow Start) 65 | * 当主机开始发送数据时,因为并不清楚网络的负荷情况,因此会先探测一下,即由小到大逐渐增大发送窗口。 66 | * 慢开始算法:通常在刚刚开始发送报文段时,先把拥塞窗口cwnd设置为一个最大报文段MSS的数值,之后每收到一个确认后都把拥塞窗口增加一个MSS的数值。 67 | * 每经过一个传输轮次,拥塞窗口cwnd就会加倍,而一个传输轮次所经历的时间其实就是往返时间RTT。 68 | * 示例图:![慢开始算法](media/OSI第四层:传输层/慢开始算法.jpg) 69 | 70 | #### 拥塞避免(Congestion Avoidance) 71 | * 为了防止拥塞窗口cwnd增长过大引起网络拥塞,需要设置一个慢开始门限ssthresh状态变量: 72 | * 当cwnd < ssthresh时,使用慢开始算法。 73 | * 当cwnd > ssthresh时,停止使用慢开始算法而改用拥塞避免算法。 74 | * 当cwnd = ssthresh时,既可使用慢开始算法,也可使用拥塞避免算法。 75 | * 拥塞避免算法:每经过一个往返时间RTT,发送方的拥塞窗口cwnd就加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。 76 | * 无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送方窗口值的一半(但不能小于2),然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。 77 | * 示例图:![拥塞避免算法](media/OSI第四层:传输层/拥塞避免算法.jpeg) 78 | 79 | #### 快重传(Fast Retransmit) 80 | * 快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认,可以使发送方尽快知道有报文段没有到达对方,而不要等到自己发送数据时才进行捎带确认。 81 | * 示例图:![快重传算法](media/OSI第四层:传输层/快重传算法.jpeg) 82 | * 接收方没有收到M3但接着收到了M4,按照快重传算法的规定,接收方应及时发送对M2的重复确认,这样做可以让发送方及早知道报文段M3没有到达接收方。发送方接着发送了M5和M6,接收方收到这两个报文后,也还要再次发出对M2的重复确认。这样,发送方共收到了接收方的四个对M2的确认,其中后三个都是重复确认。 83 | * 快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段M3,而不必继续等待M3设置的重传计时器到期。 84 | 85 | #### 快恢复(Fast Recovery) 86 | * 与快重传配合使用的还有快恢复算法,其过程有以下两个要点: 87 | * 当发送方连续收到三个重复确认,就把慢开始门限ssthresh减半,这是为了预防网络发生拥塞。 88 | * 由于发送方认为现在网络没有发生拥塞,因此不执行慢开始算法,而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。 89 | * 示例图:![快恢复算法](media/OSI第四层:传输层/快恢复算法.jpeg) 90 | 91 | ## 套接字(Socket) 92 | * Socket的基本格式为:IP地址和端口号 93 | * 每个链接的形式为:Source Socket和Destination Socket,是一条点对点、全双工的信道。 94 | * 不支持多播和广播。 95 | 96 | -------------------------------------------------------------------------------- /算法/排序算法(Sort).md: -------------------------------------------------------------------------------- 1 | # 排序算法(Sort) 2 | 3 | ## 排序算法的稳定性 4 | * 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。 5 | * 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面。 6 | 7 | ## 排序算法的分类 8 | ![分类](media/排序算法/分类.png) 9 | 10 | ## 排序算法的比较 11 | ![比较](media/排序算法/比较.png) 12 | 13 | ## 各种排序算法详解 14 | 15 | ### 冒泡排序(Bubble Sort) 16 | * 冒泡排序重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来,直到没有需要交换的元素时,排序完成。 17 | * 算法描述: 18 | 1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个; 19 | 2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数; 20 | 3. 针对所有的元素重复以上的步骤,除了最后一个; 21 | 4. 重复步骤1~3,直到排序完成。 22 | * 算法优化:设置一个标志位检测是否发生数据交换,如果没有发生数据交换,直接完成排序,这样有可能达到O(n)的时间复杂度,即数据集原本就已经排好序了。 23 | * 例子:![冒泡排序](media/排序算法/冒泡排序.gif) 24 | 25 | ### 选择排序(Select Sort) 26 | * 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 27 | * 算法描述: 28 | 1. 初始状态:无序区为R[1..n],有序区为空; 29 | 2. 第i趟排序(i=1,2,…,n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区; 30 | 3. n-1趟结束,数组有序化了。 31 | * 例子:![选择排序](media/排序算法/选择排序.gif) 32 | 33 | ### 插入排序(Insert Sort) 34 | * 通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 35 | * 算法描述: 36 | 1. 从第一个元素开始,该元素可以认为已经被排序; 37 | 2. 取出下一个元素,在已经排序的元素序列中从后向前扫描; 38 | 3. 如果该元素(已排序)大于新元素,将该元素移到下一位置; 39 | 4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置; 40 | 5. 将新元素插入到该位置后; 41 | 6. 重复步骤2~5。 42 | * 例子:![插入排序](media/排序算法/插入排序.gif) 43 | 44 | ### 希尔排序(Shell's Sort) 45 | * 1959年Shell发明,第一个突破O(n^2 )的排序算法,是简单插入排序的改进版,它先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序。 46 | * 算法描述: 47 | 1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; 48 | 2. 按增量序列个数k,对序列进行k趟排序; 49 | 3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。 50 | 4. 仅当增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。 51 | * 各种步长序列: 52 | * 希尔(Shell)的原始步长序列:1,2,4,…,N/4,N/2; 53 | * 希伯德(Hibbard)的步长序列:1,3,7,…,2^N - 1; 54 | * 克努特(Knuth)的步长序列:1,4,13,…,(3^N - 1)/2; 55 | * 例子:(Shell步长序列)![希尔排序](media/排序算法/希尔排序.gif) 56 | * 为什么希尔排序能突破O(n^2 )? 57 | * 可以用逆序数来理解,假设我们要从小到大排序,一个数组中取两个元素如果前面比后面大,则为一个逆序。容易看出排序的本质就是消除逆序数,可以证明对于随机数组,逆序数是O(n^2 )的。 58 | * 如果采用交换相邻元素的办法来消除逆序,每次正好只消除一个,因此必须执行O(n^2 )的交换次数,这就是为啥冒泡排序只能到平方级别的原因。 59 | * 反过来,基于交换元素的排序要想突破这个下界,必须执行一些比较、交换相隔比较远的元素,使得一次交换能消除一个以上的逆序,希尔、快排、堆排等等算法都是交换比较远的元素,只不过规则各不同罢了。 60 | 61 | ### 归并排序(Merge Sort) 62 | * 归并排序是一种分治算法。这个算法不断地将一个数组分为两部分,分别对左子数组和右子数组排序,然后将两个数组合并为新的有序数组。 63 | * 算法描述: 64 | 1. 把长度为n的输入序列分成两个长度为n/2的子序列; 65 | 2. 对这两个子序列分别采用归并排序; 66 | 3. 将两个排序好的子序列合并成一个最终的排序序列。 67 | * 例子:![归并排序](media/排序算法/归并排序.gif) 68 | 69 | ### 快速排序(Quick Sort) 70 | * 基本思想:挖坑填数 + 分治法 71 | * 算法描述: 72 | 1. 先从数列中取出一个数作为基准数(pivot)。 73 | 2. 将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。 74 | 3. 再对左右区间重复第二步,直到各区间只有一个数。 75 | * 例子1:![快速排序1](media/排序算法/快速排序1.gif) 76 | * 例子2:![快速排序2](media/排序算法/快速排序2.jpg) 77 | 78 | ### 堆排序(Heap Sort) 79 | * 堆排序是指利用堆这种数据结构所设计的一种排序算法。 80 | * 算法描述: 81 | 1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区; 82 | 2. 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,…,Rn-1)和新的有序区(Rn),且满足R[1,2,…,n-1]<=R[n]; 83 | 3. 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,…,Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2,…,Rn-2)和新的有序区(Rn-1,Rn)。 84 | 4. 不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。 85 | * 例子:![堆排序](media/排序算法/堆排序.gif) 86 | 87 | ### 计数排序(Counting Sort) 88 | * 计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。 89 | * 算法描述: 90 | 1. 找出待排序的数组中最大和最小的元素; 91 | 2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项; 92 | 3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加); 93 | 4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。 94 | * 当输入的元素是n个0到k之间的整数时,它的运行时间是O(n+k)。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。 95 | * 例子:![计数排序](media/排序算法/计数排序.gif) 96 | 97 | ### 桶排序(Bucket Sort) 98 | * 桶排序是计数排序的升级版,它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。 99 | * 桶排序的工作原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序)。 100 | * 算法描述 101 | 1. 设置一个定量的数组当作空桶; 102 | 2. 遍历输入数据,并且把数据一个一个放到对应的桶里去; 103 | 3. 对每个不是空的桶进行排序; 104 | 4. 从不是空的桶里把排好序的数据拼接起来。 105 | * 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 106 | * 尽量减少桶内数据的数量是提高效率的唯一办法,因此我们需要尽量做到下面两点: 107 | * 映射函数能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。 108 | * 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的比较排序操作。当然,做到这一点很不容易,数据量巨大的情况下,映射函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间和空间的权衡问题了。 109 | 110 | ### 基数排序(Radix Sort) 111 | * 基数排序的思想就是将待排数据中的每组关键字依次进行桶排序。 112 | * 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。 113 | * 算法描述: 114 | 1. 取得数组中的最大数,并取得位数; 115 | 2. arr为原始数组,从最低位开始取每个位组成radix数组; 116 | 3. 对radix进行计数排序(利用计数排序适用于小范围数的特点)。 117 | * 例子:![基数排序](media/排序算法/基数排序.gif) 118 | 119 | 120 | -------------------------------------------------------------------------------- /软件设计/软件设计.md: -------------------------------------------------------------------------------- 1 | # 软件设计 2 | 3 | ## 内聚与耦合 4 | 5 | ### 模块的概念 6 | 模块是一系列语句组成的,由标识符组成的边界元素来界定的。比如面向对象语言中的一个类、一个方法;也如面向过程中的函数。 7 | 8 | ### 内聚 9 | 内聚是指模块内的交互程度,以下几种内聚程度由低到高。 10 | 11 | * 偶然内聚:组件的部件是不相关的,只是简单地绑定成单个组件。 12 | * 逻辑内聚:把相似的功能(类如输入,错误处理)放在一块,通过传递一个参数来决定是哪一个功能来执行。 13 | * 时间内聚:所有的语句在同一时刻被激活,就像电脑关机的时候,其他所有的程序都要被关闭。 14 | * 过程内聚:简单地把一系列过程关联在一起。 15 | * 通信内聚:操作相同的输入数据或者输出相同的输出数据,可能产生多种功能。 16 | * 顺序内聚:用一个部分的输出作为另一部分的输入,可能包含几个功能或部分不同的功能。 17 | * 信息内聚:执行多个功能,每个函数都有自己的入口点,每个函数都有独立的代码,所有的功能都在相同的数据结构上执行。不同于逻辑衔接,因为功能没有交织在一起。 18 | * 功能内聚:每一部分都需要执行一个单一的功能。例如,计算平方根或排序数组。 19 | 20 | ### 耦合 21 | 耦合是指模块之间的交互程度,以下几种耦合程度由高到低。 22 | 23 | * 内容耦合:一个模块直接操作操作另外一个模块中的内容。 24 | * 公共耦合:就像一个类中的全局变量类中的模块都直接操作这个全局变量。 25 | * 控制耦合:通过控制标志(作为参数或变量),一个模块控制另一个模块的处理步骤的顺序。 26 | * 印记耦合:一组模块通过参数表传递记录信息,这组模块共享了这个记录,它是某一数据结构的子结构,而不是简单变量。这要求这些模块都必须清楚该记录的结构,并按结构要求对此记录进行操作。 27 | * 数据耦合:如果一个模块访问另一个模块时,彼此之间是通过数据参数(不是控制参数、公共数据结构或外部变量)来交换输入、输出信息的,则称这种耦合为数据耦合。 28 | 29 | ## 设计原则 30 | 31 | ### 开闭原则(Open-Closed Principle,OCP) 32 | * 一个软件实体应当对扩展开放,对修改关闭。因为客户的需求是不稳定的,我们应该通过扩展已有的软件系统而不是通过修改软件系统来满足客户的需求。 33 | * 具体表现为已有的模块,特别是抽象层的模块不能修改,保证软件系统的稳定性和延续性。 34 | * 实现开闭原则的关键是抽象化,在面向对象的编程语言里,可以给系统定义出一套相对较为固定的抽象设计,此设计允许无穷无尽的行为在实现层被实现。 35 | * 在语言里,可以给出一个或多个抽象类或者接口,规定出所有的具体类必须提供的方法的特征作为系统设计的抽象层。这个抽象层预见了所有的可扩展性,因此,在任何扩展情况下都不会改变。这就使得系统的抽象不需要修改,从而满足了开闭原则的第二条,对修改关闭。同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了开闭原则的第一条。 36 | 37 | ### 里氏代换原则(Liskov Substitution Principle,LSP) 38 | * 子类型必须能够替换它们的基类型,而反过来的代换则不成立。 39 | * 当两个具体类关系违反里氏代换原则时,有两种解决方法: 40 | * 一种是抽象出一个基类,作为这两个类的父类; 41 | * 另一种是建立组合或聚合关系。 42 | * 不要为了使用某些类的方法(功能)而滥用继承。 43 | 44 | ### 依赖倒置原则(Dependence Inversion Principle,DIP) 45 | * 具体要依赖于抽象,而不是抽象依赖于具体。 46 | * 要针对接口编程,不要针对实现编程。同样,在处理类之间的耦合关系时,尽量使用抽象耦合的形式。 47 | * 里氏替换原则是依赖倒转原则的基础。工厂模式、模板模式、迭代子模式都是对依赖倒转原则的体现。 48 | 49 | ### 接口隔离原则(Interface Segregation Principle,ISP) 50 | * 使用多个专门的接口而不使用单一的总接口。换而言之,一个类对另外一个类的依赖性应当是建立在最小接口上的。 51 | * 过于臃肿的接口是对接口的污染,不应该强迫客户依赖于它们不用的方法,每一个接口应该是一种角色,不多不少,不干不该干的事,该干的事都要干。 52 | * 为同一个角色提供宽、窄不同的接口,以应对不同类的需求。 53 | 54 | ### 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP) 55 | * 就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。 56 | * 简而言之,要尽量使用合成/聚合,而尽量不要使用继承。 57 | * 要区分has a和is a的问题。 58 | 59 | ### 迪米特法则(Law of Demeter,LoD) 60 | * 只与直接的朋友们通信,而不要和陌生人说话。 61 | * 如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另外一个类的某一个方法,可以通过第三者转发这个调用。 62 | 63 | ## 设计模式 64 | 65 | ### 单例模式(Singleton) 66 | 在Java中,单例模式能保证在JVM中只存在一个单例对象的实例,有如下几种实现方法: 67 | 68 | #### 饿汉式 69 | ```java 70 | public final class Singleton { 71 | private static Singleton instance = new Singleton(); 72 | private Singleton() {} 73 | public static Singleton getInstance() { 74 | return instance; 75 | } 76 | } 77 | ``` 78 | 79 | * 每个对象在没有使用之前就已经初始化了。 80 | * 带来性能问题:当对象很大时,在没有使用它之前,就把它加载到内存中,浪费内存空间。 81 | 82 | #### 懒汉式 83 | ```java 84 | public final class Singleton { 85 | private static Singleton instance; 86 | private Singleton() {} 87 | public static Singleton getInstance() { 88 | if (instance == null) { 89 | instance = new Singleton(); 90 | } 91 | return instance; 92 | } 93 | } 94 | ``` 95 | 96 | * 使用延迟加载来保证对象在没有使用之前不会初始化。 97 | * 线程不安全,因为在多个线程可能同时运行到判断instance为null,于是同时初始化。 98 | 99 | #### 懒汉式 + synchronized 100 | ```java 101 | public final class Singleton { 102 | private static Singleton instance; 103 | private Singleton() {} 104 | public static synchronized Singleton getInstance() { 105 | if (instance == null) { 106 | instance = new Singleton(); 107 | } 108 | return instance; 109 | } 110 | } 111 | ``` 112 | 113 | * getInstance()方法加同步锁,解决线程安全问题。 114 | * 性能问题:同步的代价必然会一定程度地降低程序并发度。 115 | 116 | #### 双重检查锁 117 | ```java 118 | public final class Singleton { 119 | private static Singleton instance; 120 | private Singleton() {} 121 | public static Singleton getInstance() { 122 | if (instance == null) { 123 | synchronized (Singleton.class) { 124 | if (instance == null) { 125 | instance = new Singleton(); 126 | } 127 | } 128 | } 129 | return instance; 130 | } 131 | } 132 | ``` 133 | 134 | * 这种写法在解决线程安全问题的同时,保证了程序并发度。 135 | 136 | ### 工厂模式(Factory) 137 | 建立一个工厂类,对实现了同一接口的一些类进行实例化。 138 | 139 | #### 简单工厂模式 140 | * 普通工厂方法模式:先实例化工厂类,再调用工厂对象的创建方法,将类名字符串作为参数传入,返回值就是创建好的对象。 141 | * 多个工厂方法模式:在普通工厂方法模式中,如果传递的字符串参数出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别对应不同对象的创建。 142 | * 使用静态方法:将多个工厂方法模式里的方法都改为静态的,不需要创建工厂实例,直接调用工厂类的静态方法即可。 143 | 144 | #### 工厂方法模式 145 | * 简单工厂模式有一个问题:类的创建依赖于工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,违背了闭包原则。 146 | * 工厂方法模式是指,创建一个工厂接口和创建多个工厂实现类,这样一旦需要增加新的功能,直接增加新的工厂实现类,而不需要修改之前的代码。 147 | 148 | #### 抽象工厂模式 149 | * 抽象工厂模式是工厂方法模式的一种升级,它提供一个接口,可以创建一个对象集合,这个集合中的对象组合起来可以实现特定功能,该接口可以有多种实现。 150 | * 工厂方法模式和抽象工厂模式的区别:工厂方法模式是针对一个抽象产品类来生产对应的对象,而抽象工厂模式是针对多个抽象产品类来生产对应的对象集合。 151 | 152 | 153 | -------------------------------------------------------------------------------- /基于多核的并行编程/Ch1-2 并行编程基础.md: -------------------------------------------------------------------------------- 1 | # Ch1-2 并行编程基础 2 | 3 | ## 综述 4 | * 并行编程:对给定算法构造并行程序的活动。 5 | * 编程模型:程序员在开发一个并行程序时所见到和使用的模型。 * 自然模型:一个特定并行计算机平台所提供的、用户可见的最底层的编程模型。 * 高层编程模型:在自然模型上加以实现。 6 | * 并行编程进展 7 | * 已开发了许多并行算法,尽管大多数算法基于PRAM模型,但其中某些经过修正后可以实用。 8 | * 并行算法已逐渐被用户所接受。 9 | * 自然模型集中趋向于两种模型:单地址空间的共享变量模型(适用于PVP、SMP和DSM)和多地址空间的消息传递模型(适用于MPP和Cluster)。SIMD模型已经从主流、通用计算机淡出。 10 | * 高层并行编程模型集中趋向于三种标准模型:数据并行、消息传递和共享变量。此外还有一种模型,用户只需编写顺序程序,其中的蕴式并行性由并行化编译器(如Kap)进行析取。 11 | 12 | ## 显式并行和隐式并行 13 | * 显式并行 14 | * 在源程序中由程序员使用专用语言构造、编译器命令或库函数对并行性加以显式说明。 15 | * 包括共享变量模型、消息传递模型和数据并行模型。 16 | * 隐式并行 17 | * 程序员不显式地说明并行性,而是让编译器或运行支持系统自动加以开发。 18 | * 包括并行化编译器、运行时间并行化。 19 | 20 | ## 并行化方法 21 | 22 | ### 环境支持(扩展Fortran或C) 23 | * 例程库:除了在串行语言中可用的标准库外,加入一组新的库函数,以支持并行化和交互操作。例如MPI库、POSIX Pthread多线程库。 24 | * 新构造:扩展程序设计语言使其具有某些新构造,以支持并行化和交互。例如Fortran 90中的密集数据操作。 25 | * 编译器命令/预处理:程序设计语言不变,但加入编译器命令(pragmas)的格式化注解。 26 | 27 | ### 举例 28 | * 串行代码 29 | 30 | ```C 31 | for (i=0; i`标签的`scope`属性 39 | * 通过注解:`@Scope("singleton")` 40 | 41 | ##### Singleton 42 | * 每一个Bean的实例只会被创建一次,而且Spring容器在整个应用程序生存期中都可以使用该实例,相当于单例模式。 43 | * Spring默认创建的所有Bean的作用域都是Singleton。 44 | 45 | ##### Prototype 46 | * 每次获取Bean实例时都会新创建一个实例对象,类似new操作符。 47 | 48 | ##### Request和Session 49 | * 在Spring2.5中专门针对Web应用程序引进了Request和Session这两种作用域。 50 | * Request作用域:每次HTTP请求到达时,会创建一个新的Request作用域的Bean实例,该实例装载了这次请求的数据(如参数等),该Bean实例仅在当前HTTP Request内有效且唯一,而其他请求HTTP请求则创建新Bean的实例互不干扰,这个Bean实例会在处理请求结束销毁。 51 | * Session作用域:每当创建一个新的HTTP Session时就会创建一个Session作用域的Bean,并该实例Bean伴随着本次Session的存在而存在。 52 | 53 | ##### globalSession作用域 54 | * 类似Session作用域,相当于全局变量。 55 | 56 | #### Bean的循环依赖问题 57 | 58 | ##### 简介 59 | * 循环依赖其实就是循环引用,也就是两个或则两个以上的Bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。 60 | * 这样的情况,这些类单独使用时不会出问题,但在使用Spring管理时可能会导致BeanCurrentlyInCreationException等异常,表示Spring解决不了该循环依赖。 61 | 62 | ##### 分类 63 | * 构造方法循环引用:A的构造方法中引用了B,同时B的构造方法中引用了A。 64 | * setter循环引用:A的构造方法中引用了B,同时B的某个setter引用了A,以及反之;或者A的某个setter引用了B,同时B的某个setter引用了A,以及反之。 65 | 66 | ##### 解决前提 67 | * 循环引用的Bean的scope都是单例的,并且它们没有显式指明不需要解决循环依赖,此外还要求这些Bean没有被代理过。 68 | * 因为Spring容器不缓存prototype的Bean,所以prototype的循环引用Bean无法完成依赖注入。 69 | * 构造方法中的循环依赖Spring仍然没法解决。 70 | 71 | ##### 解决方案 72 | * Java基于引用传递,当获取到对象的引用时,对象的成员属性可以延后设置。 73 | * Spring单例对象的初始化分为以下三步:createBeanInstance()实例化、populateBean()填充属性数据、initializeBean()调用配置文件中指定的init方法或AfterPropertiesSet方法。 74 | * Spring单例对象使用三级缓存来管理,三级缓存包括:singletonObjects单例对象的cache、earlySingletonObjects提前曝光的单例对象的cache、singletonFactories单例对象工厂的cache。 75 | * getSingleton的时候,首先尝试从singletonObjects中获取,如果获取不到就会尝试从earlySingletonObjects中获取,如果获取不到就会尝试用singletonFactory.getObject()获取,如果获取到了就移除对应的singletonFactory,并将对应的singletonObject放入earlySingletonObjects中提前曝光,这样就可以获取到一个提前曝光的对象实例从而解决循环引用了。 76 | 77 | #### Bean的延迟加载 78 | * 默认情况下Spring容器在启动阶段就会创建所有Bean,这个过程被称为预先初始化,这样的好处是可以尽早发现配置错误,但如果存在大量Bean需要初始化,就会引起Spring容器启动缓慢,这时候我们会希望只有在用到某个Bean的时候再创建其实例对象,以减少内存消耗。 79 | * 一些特定的Bean没必要在Spring容器启动阶段就创建,比如Hibernate的SessionFactory等,延迟加载它们会让Spring容器启动更轻松些,从而也减少没必要的内存消耗。 80 | * 在Spring的XML配置文件中使用``的`lazy-init`属性可以配置该Bean是否延迟加载;如果需要配置整个XML文件的Bean都延迟加载则使用`default-lazy-init`属性,需要注意`lazy-init`属性会覆盖`default-lazy-init`属性。 81 | * 配置了延迟加载,在以下情况下仍会触发Bean的创建: 82 | * 从一个已创建的Bean引用另外一个Bean; 83 | * 显式地查找一个Bean。 84 | 85 | ### 面向切面编程AOP 86 | AOP就是纵向的编程,如业务1和业务2都需要一个共同的操作,与其往每个业务中都添加同样的代码,不如写一遍代码,让两个业务共同使用这段代码。所以AOP把所有共有代码全部抽取出来,放置到某个地方集中管理,然后在具体运行时,再由容器动态织入这些共有代码。 87 | 88 | #### 相关概念 89 | * 切面(Aspect):其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类,之所以能被AOP容器识别成切面,是在配置中指定的。 90 | * 通知(Advice):是切面的具体实现,在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。 91 | * 连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但Spring只支持方法级的连接点。 92 | * 切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。 93 | * 目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能代码等待AOP容器的切入。 94 | * 代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。 95 | * 织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器;发生在类装载期,就要求有一个支持AOP实现的特殊类装载器;只有发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现。 96 | 97 | #### 通知类型 98 | 99 | ##### 前置通知 100 | * 通过@Before注解进行标注,该通知在目标函数执行前执行,并且可以直接传入切点表达式。 101 | * 参数是JoinPoint,是Spring提供的静态变量,可以通过它获取目标对象的信息,如类名称、方法参数、方法名称等,该参数是可选的。 102 | 103 | ##### 后置通知 104 | * 通过@AfterReturning注解进行标注,该通知在目标函数执行完成后执行,并可以获取到目标函数最终的返回值returnVal。 105 | * 当目标函数没有返回值时,returnVal将返回null。 106 | * 必须通过returning = “returnVal”注明参数的名称而且必须与通知函数的参数名称相同。 107 | * 请注意,在任何通知中这些参数都是可选的,需要使用时直接填写即可,不需要使用时,可以完成不用声明出来。 108 | 109 | ##### 异常通知 110 | * 通过@AfterThrowing注解进行标注,该通知只有在异常时才会被触发,并由throwing来声明一个接收异常信息的变量。 111 | * 同样异常通知也可以用JoinPoint参数,需要时加上即可。 112 | 113 | ##### 最终通知 114 | * 通过@After注解进行标注,该通知有点类似于finally代码块,只要应用了无论什么情况下都会执行。 115 | * 参数也是JoinPoint。 116 | 117 | ##### 环绕通知 118 | * 通过@Around注解进行标注,该通知既可以在目标方法前执行也可在目标方法之后执行,更重要的是环绕通知可以控制目标方法是否指向执行。 119 | * 但即使如此,我们应该尽量以最简单的方式满足需求,在仅需在目标方法前执行时,应该采用前置通知而非环绕通知。 120 | * 第一个参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来执行目标函数,proceed()的返回值就是环绕通知的返回值。 121 | * 同样的,ProceedingJoinPoint对象也可以获取目标对象的信息,如类名称、方法参数、方法名称等。 122 | 123 | #### 织入类型 124 | 对于织入这个概念,可以简单理解为把切面应用到目标函数的过程,这个过程一般分为动态织入和静态织入。 125 | 126 | ##### 静态织入 127 | * AspectJ采用静态织入的方式,主要采用编译期织入,即在编译期间使用AspectJ的acj编译器(类似javac)把aspect类编译成字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。 128 | * 除了编译期织入,还存在链接期(编译后)织入,即将aspect类和java目标类同时编译成字节码文件后,再进行织入处理,这种方式比较有助于已编译好的第三方jar和Class文件进行织入操作。 129 | 130 | ##### 动态织入 131 | * 动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理和CGLIB的动态代理。 132 | * Java JDK的动态代理:底层通过反射实现,要求目标对象有接口类。 133 | * CGLIB的动态代理:底层通过继承实现,因此可以减少没必要的接口。 134 | 135 | #### 组成部分 136 | * Spring使用AOP配置事务管理由以下三个部分组成:DataSource、TransactionManager和代理机制。 137 | * 无论哪种配置方式,一般变化的只是代理机制这部分,而DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,比如使用Hibernate进行数据访问时,DataSource实际为SessionFactory,TransactionManager的实现为HibernateTransactionManager。 138 | * 代理机制分为Java JDK的动态代理和CGLIB的动态代理。 139 | 140 | ### SpringMVC 141 | 1. 客户端请求提交到DispatcherServlet。 142 | 2. 由DispatcherServlet控制器查询HandlerMapping,找到并分发到指定的Controller中。 143 | 3. Controller调用业务逻辑处理后,返回ModelAndView。 144 | 4. DispatcherServlet查询一个或多个ViewResolver视图解析器,找到ModelAndView指定的视图。 145 | 5. 视图负责将结果显示到客户端。 146 | 147 | ## Servlet 148 | * Servlet是用来处理客户端请求并产生动态网页内容的Java类。Servlet主要是用来处理或者是存储HTML表单提交的数据,产生动态内容,在无状态的HTTP协议下管理状态信息。 149 | * Servlet链是把一个Servlet的输出发送给另一个Servlet的方法。第二个Servlet的输出可以发送给第三个Servlet,依次类推。链条上最后一个Servlet负责把响应发送给客户端。 150 | 151 | ### 生命周期 152 | * 加载Servlet:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例。 153 | * 初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象。 154 | * 处理服务:当浏览器访问Servlet的时候,Servlet会调用service()方法处理请求。 155 | * 销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁。 156 | * 卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作。 157 | 158 | ### 相关API 159 | 160 | #### doGet与doPost方法的两个参数 161 | * HttpServletRequest:封装了与请求相关的信息 162 | * HttpServletResponse:封装了与响应相关的信息 163 | 164 | #### 获取页面元素的几种方式 165 | * request.getAttribute():一般用于获取request域对象的数据,如在跳转之前把数据使用setAttribute来放到request对象上。 166 | * request.getParameter():返回客户端的请求参数的值。 167 | * request.getParameterNames():返回所有可用属性名的枚举。 168 | * request.getParameterValues():返回包含参数的所有值的数组。 169 | 170 | ### 线程安全 171 | * 由于Servlet是单例的,当多个用户访问Servlet的时候,服务器会为每个用户创建一个线程,当多个用户并发访问Servlet共享资源的时候就会出现线程安全问题。 172 | * 原则:如果一个Servlet需要多个用户共享,则应当在访问该Servlet的时候,加同步机制synchronized或Lock。 173 | 174 | ### 常见问题 175 | 176 | #### JSP与Servlet的区别 177 | * Servlet是服务器端的程序,动态生成HTML页面发送到客户端,但是这样程序里会有很多out.println(),Java与HTML语言混在一起很乱,所以后来Sun公司推出了JSP。 178 | * 其实JSP就是Servlet,每次运行的时候JSP都首先被编译成Servlet文件,然后再被编译成.class的字节码文件运行。有了JSP,在MVC项目中Servlet不再负责动态生成页面,转而去负责控制程序逻辑的作用,控制JSP与Java Bean之间的交互。 179 | 180 | #### JSP中动态include和静态include的区别 181 | * include指令用于把另一个页面包含到当前页面中,在转换成Servlet时包含进去的。 182 | * 动态include用JSP标签(``)实现,总是会检查所含文件中的变化,适合用于包含动态页面,并且可以带参数。 183 | * 静态include用include伪码(`<%@ include file="included.html" %>`)实现,不会检查所含文件的变化,适用于包含静态页面。 184 | 185 | #### 转发forward和重定向redirect的区别 186 | 187 | ##### 实际发生位置不同 188 | * 转发是由服务器进行跳转的,浏览器地址栏不变。转发只有一次的HTTP请求,一次转发中request和response对象都是同一个。 189 | * 重定向是由浏览器进行跳转的,浏览器地址栏会发生变化。重定向的原理是由response的状态码和Location头的组合而实现的,所以实现重定向会发出两个HTTP请求,request域对象是无效的,因为它不是同一个request对象。 190 | 191 | ##### 传递数据的类型不同 192 | * 转发的request对象可以传递各种类型的数据(使用request.getAttribute()接口)。 193 | * 重定向只能传递字符串(HTTP Request的请求参数)。 194 | 195 | ##### 跳转的时间不同 196 | * 转发:执行到跳转语句时就会立刻跳转。 197 | * 重定向:整个页面执行完之后才执行跳转。 198 | 199 | ##### 应用场景 200 | * 转发:访问Servlet处理业务逻辑,然后转发到JSP显示处理结果,浏览器地址栏不变。 201 | * 重定向:提交表单,处理成功后重定向到另一个JSP,防止表单重复提交,浏览器地址栏变了。 202 | 203 | ## JDBC 204 | * JDBC是Java对数据库操作的封装,允许开发者用Java操作数据库,而不需要关心底层特定数据库的细节。 205 | * JDBC驱动提供了特定厂商对JDBC API接口类的实现,驱动必须要提供java.sql包下面这些类的实现:Connection,Statement,PreparedStatement,CallableStatement,ResultSet和Driver。 206 | * Class.forName方法用来载入跟数据库建立连接的驱动。 207 | * 数据库连接池:像打开关闭数据库连接这种和数据库的交互可能是很费时的,尤其是当客户端数量增加的时候,会消耗大量的资源。所以可以在应用服务器启动的时候建立很多个数据库连接并维护在一个池中。连接请求由池中的连接提供。在连接使用完毕以后,把连接归还到池中,以用于满足将来更多的请求。 208 | 209 | ## Hibernate 210 | 211 | ## MyBatis 212 | 213 | -------------------------------------------------------------------------------- /Java/Java集合.md: -------------------------------------------------------------------------------- 1 | # Java集合 2 | 3 | ## Java常用集合 4 | * Collection接口:代表一个对象集合。 5 | * List接口:继承Collection接口,存放可重复、有序的元素。 6 | * Queue接口:继承Collection接口,模拟队列操作,采用先进先出的方式组织数据,但优先队列PriorityQueue是一个例外。Deque接口继承Queue接口,模拟双端队列,可以被作为栈来使用。 7 | * Set接口:继承Collection接口,存放非重复、无序的元素,使用`equals()`方法比较两个对象是否相同,如果返回true,则两个对象的`hashCode()`返回值也应该相同。 8 | * Map接口:没有继承Collection接口,它可以把键(key)映射到值(value)的对象,键不能重复。 9 | 10 | ### Collection和Collections 11 | * Collection是代表集合的接口,List、Set等集合接口都继承它(Map接口除外)。 12 | * Collections是工具类,提供了排序、同步等很多实用方法。 13 | * 排序:`Collections.sort(Collection collection)` 14 | * 同步List:`Collections.synchronizedList(List list)` 15 | * 同步Map:`Collections.synchronizedMap(Map map)` 16 | * 同步Set:`Collections.synchronizedSet(Set set)` 17 | * Collections的同步实现是指自身实现List/Map/Set的接口,然后在内部维护一个对应的List/Map/Set的实例,synchronized地调用这些实例来实现接口。 18 | 19 | ### Comparable和Comparator 20 | * Comparable接口:只包含一个`compareTo()`方法,这个方法可以个给两个对象排序。具体来说,它返回负数、0、正数来表明输入对象小于、等于、大于自身对象。 21 | * Comparator接口:包含`compare()`和`equals()`两个方法。`compare()`方法用来给两个输入参数对象排序,返回负数、0、正数表明第一个参数对象是小于、等于、大于第二个参数对象。`equals()`方法需要一个对象作为参数,它用来决定输入参数是否和Comparator相等。只有当输入参数也是一个Comparator并且输入参数和当前Comparator的排序结果是相同的时候,这个方法才返回true。 22 | 23 | ## 迭代器 24 | 25 | ### Iterator和Enumeration 26 | * Enumeration的速度是Iterator的两倍,也使用更少的内存。 27 | * Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改该集合。 28 | * Iterator允许调用者从集合中移除元素,而Enumeration不能做到。 29 | 30 | ### Iterator和ListIterator 31 | * Iterator可以遍历Set和List,而ListIterator只能遍历List。 32 | * Iterator只能前向遍历,而ListIterator可以双向遍历。 33 | * ListIterator从Iterator接口继承,添加了一些额外的功能,比如:添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 34 | 35 | ### fail-fast和fail-safe 36 | * fail-fast机制 37 | * 在遍历一个集合时,当集合结构被修改,会抛出ConcurrentModificationException。 38 | * 实现方法:比较modcount和expectModcount,如果不同则抛出异常。 39 | * fail-safe机制 40 | * 将原集合复制后再开始遍历,任何对原集合的修改不会影响到复制后的集合,因此不会抛出ConcurrentModificationException。 41 | * 存在两个问题:需要复制集合,产生大量的无效对象,开销大;无法保证读取的数据是目前原始数据结构中的数据。 42 | * Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的集合类都为fail-safe的。 43 | 44 | ## List接口 45 | List接口继承Collection接口,存放可重复、有序的元素。 46 | 47 | ### ArrayList和Array 48 | * Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。 49 | * Array大小是固定的,ArrayList的大小是动态变化的。 50 | * ArrayList提供了更多的方法和特性,比如:addAll,removeAll等。 51 | 52 | ### ArrayList和LinkedList 53 | * ArrayList是基于索引的数据接口,它的底层是数组,可以以O(1)时间复杂度对元素进行随机访问。 54 | * LinkedList是以双链表的形式存储它的数据,查找的时间复杂度是O(n),但插入和删除元素的时间复杂度是O(1),因为当元素被添加到集合任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引。 55 | 56 | ### ArrayList和Vector 57 | * 两者都继承了抽象类AbstractList,内部实现都是数组,迭代器都是fast-fail的,都允许存储null。 58 | * Vector是线程安全的,而ArrayList是非线程安全的。然而如果希望在迭代的时候对列表进行改变,应该使用CopyOnWriteArrayList。 59 | * 需要扩容时,ArrayList扩展到原来的1.5倍大小,Vector扩展到原来的2倍大小,而且Vector可以自由设置扩展的比例。 60 | 61 | ### Vector和CopyOnWriteArrayList 62 | * Vector是通过给`add()`、`remove()`、`set()`、`get()`等方法加synchronized来进行线程同步,但是没有控制到遍历操作,所以Vector也不能线程安全的遍历。 63 | * CopyOnWriteArrayList是java.util.concurrent包中的一个实现了List接口的类,CopyOnWrite的意思是在对List进行修改的时候复制一份List并在这份List上进行修改,最后将原List的引用指向修改后的List。遍历也是一样的思想。 64 | * CopyOnWriteArrayList使用ReentrantLock和volatile实现同步,相对于Vector的优势是并发读的效率高,但缺点在于读的可能不是最新值,而且需要创建新数组,会占用额外空间。 65 | 66 | ### Stack 67 | * Stack继承Vector,它实现了一个标准的后进先出的栈。 68 | * Stack只定义了默认构造函数,用来创建一个空栈。 69 | * Stack除了包括由Vector定义的所有方法外,还定义了栈需要的方法:`boolean empty()`、`Object peek()`、`Object pop()`、`Object push(Object element)`、`int search(Object element)`。 70 | 71 | ## Queue接口和Deque接口 72 | * Queue接口模拟队列,采用先进先出的方式组织数据,但优先队列PriorityQueue是一个例外。 73 | * Deque接口继承Queue接口,模拟双端队列,可以被作为栈来使用。 74 | 75 | ### LinkedList 76 | * Java中,LinkedList实现了Deque接口,可以作为一个双端队列来使用。 77 | * 选择LinkedList来实现队列的原因是:LinkedList进行插入、删除操作效率较高。 78 | 79 | ### PriorityQueue 80 | * PriorityQueue实现了Queue接口,基于堆实现,是一个非FIFO队列,没有容量限制,可以自动扩容,初始化时可以指定初始大小。 81 | * 在实例化优先队列时,可以在构造函数中提供Comparator比较器,然后队列中的项目顺序将根据提供的比较器来决定。如果没有提供比较器,则默认将使用Collection的自然顺序(Comparable)来排序元素。 82 | * 优先队列中不允许存null元素。 83 | * 如果有多个对象拥有相同的优先级,则它们出队的顺序是随机的。 84 | * 优先队列是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境。 85 | 86 | ### ArrayDeque 87 | * ArrayDeque实现了Deque接口,基于可变数组实现,没有容量限制,可以自动扩容,初始化时可以指定初始容量,默认为16。 88 | * ArrayDeque中不允许存null元素。 89 | * ArrayDeque底层使用环形数组存储元素,使用了两个索引表示当前数组的状态,分别是head和tail。其中head是头部元素的索引,tail是尾部元素的下一位的索引。因为是环形数组,所以索引0是索引length-1的下一位。 90 | * ArrayDeque对数组的大小(即队列的容量)有特殊的要求,必须是2^n。这是因为在容量保证为2^n的情况下,仅通过按位与运算`(tail+1)&(elements.length-1)`就可以完成环形索引的计算,而不需要进行边界的判断,在实现上更为高效。 91 | * 在每次添加元素后,如果head索引和tail索引相遇,则说明数组空间已满,需要进行扩容,扩容后的数组容量是原来的两倍,这也可以满足容量必须是2的幂次方的要求。 92 | * ArrayDeque是非线程安全的,当多个线程同时使用的时候,需要手动同步。 93 | 94 | ### ConcurrentLinkedQueue 95 | ConcurrentLinkedQueue是一个基于链表实现的线程安全的无界队列,它使用非阻塞的方式实现线程安全,即使用自旋+CAS指令。 96 | 97 | ### ArrayBlockingQueue和LinkedBlockingQueue 98 | * 这两个队列都实现了BlockingQueue接口,是阻塞队列,可以阻塞添加和阻塞删除。 99 | * 阻塞添加:当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。 100 | * 阻塞删除:在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作。 101 | * ArrayBlockingQueue是一个用数组实现的有界阻塞队列,通过可重入锁ReentrantLock和条件队列Condition实现同步,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别。 102 | * 公平访问:被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。 103 | * 非公平访问:当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,谁先抢到谁就执行,没有固定的先后顺序。 104 | * LinkedBlockingQueue是一个用链表实现的有界阻塞队列,其大小默认值为Integer.MAX_VALUE,所以在实例化时建议手动设置大小,避免因队列容量过大而造成内存溢出。与ArrayBlockingQueue不同的是,LinkedBlockingQueue使用了takeLock和putLock这两个ReentrantLock来实现同步,也就是说,添加和删除操作不是互斥操作,可以同时进行,这样可以大大提高吞吐量。 105 | 106 | ## Map接口 107 | Map接口没有继承Collection接口,其中数据以无序键值对形式存在,键不可重复,值可以重复。 108 | 109 | ### HashMap 110 | * HashMap在静态内部类Map.Entry中存储key-value对,使用每个对象的`hashCode()`和`equals()`方法计算其对应Hash值,解决冲突的方法是链表探测法,用LinkedList实现。 111 | * HashMap的API调用流程: 112 | * 调用put方法时,HashMap先调用key对象的`hashCode()`来算出其Hash值,再到数组对应其Hash值的位置的链表里使用`equals()`方法查找该entry是否已存在,如果不存在则会创建一个新的entry来保存。 113 | * 调用get方法时,HashMap先调用key对象的`hashCode()`来算出其Hash值,再到数组对应其Hash值的位置的链表里查找,使用`equals()`方法找出正确的entry并返回它的值。 114 | * HashMap的容量、负荷系数、阀值和rehash 115 | * HashMap默认的初始容量是32,负荷系数是0.75。HashMap的容量总是2的幂,因为这样才能使用与运算来计算元素下标:`hashcode & (length-1)`。 116 | * 阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行rehash,使用更大的容量来存储。 117 | * 所以如果预先知道需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,应该使用正确的容量和负荷系数来初始化HashMap。 118 | * HashMap线程不安全的体现: 119 | * rehash死循环:Java中HashMap解决冲突的办法是用链表,在rehash的时候需要把链表转移,转移时会逆序,多线程情况下会导致单链表的死循环。 120 | * fail-fast机制:使用迭代器遍历HashMap过程中,如果有其他线程修改了HashMap,那么会抛出ConcurrentModificationException。 121 | * HashMap在JDK1.8中的优化:HashMap采用链表法解决冲突,但当冲突过多使得链表过长时(默认是链表长度大于8时),把链表换成红黑树。 122 | 123 | ### HashMap和HashTable 124 | * HashMap和HashTable都实现了Map接口,都是键值对保存数据的方式。 125 | * HashMap允许key和value为null,而HashTable不允许。 126 | * HashMap不是线程安全的类,而HashTable是线程安全的类。 127 | * HashMap提供对key的Set进行遍历,因此它是fail-fast的,而HashTable提供对key的Enumeration进行遍历,它不支持fail-fast。 128 | * HashTable被认为是个遗留的类,应该使用ConcurrentHashMap代替它。 129 | 130 | ### HashTable和ConcurrentHashMap 131 | * HashTable线程安全策略的实现代价大,简单粗暴,`get()`/`put()`等所有相关操作都是synchronized的,相当于给整个哈希表加了一把大锁。多线程访问的时候,只能同时有一个线程访问或操作该对象,其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。 132 | * ConcurrentHashMap采用了非常精妙的"分段锁"策略,将原来的数组分成多个Segment,一个Segment就是一个子哈希表,每个Segment里维护了一个HashEntry数组,因此对于不同Segment的数据进行操作是不用考虑锁竞争的。 133 | 134 | ### LinkedHashMap 135 | * HashMap的迭代顺序不是元素插入顺序,而LinkedHashMap通过维护一个存Entry的双向链表,从而保证元素迭代的顺序,该迭代顺序默认是插入顺序,也可以是访问顺序,缺点是增加了时间和空间上的开销。 136 | * LinkedHashMap继承HashMap,所以跟HashMap一样允许key和value为null,而且也一样不是线程安全的类。 137 | 138 | ### TreeMap 139 | * TreeMap继承于AbstractMap,实现了NavigableMap接口,它可以确保集合元素处于排序状态,使用树形结构(算法书中的红黑树)实现,要求其中的元素是可排序的,有两种排序方式: 140 | * 自然排序(Comparable接口),也是默认排序。 141 | * 比较器排序(Comparator接口),构造方法中作为参数传入。 142 | * TreeMap基本操作的时间复杂度为O(log(n))。 143 | * TreeMap不是线程安全的,它的迭代器是fail-fast的。 144 | 145 | ### EnumMap 146 | * EnumMap是一个与枚举类一起使用的Map实现,其中所有key都必须是指定枚举类的枚举值,创建EnumMap时必须显式或隐式指定它对应的枚举类。 147 | * EnumMap在内部以数组形式保存,根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对的次序,当程序通过`keySet()`、`entrySet()`、`values()`等方法来遍历EnumMap时即可看到这种顺序。 148 | * EnumMap不允许使用null作为key,但允许使用null作为value,如果试图使用null作为key将抛出NullPointerException异常。 149 | 150 | ### IdentityHashMap 151 | * IdentityHashMap使用Hash算法实现Map接口,但它的key可以重复。 152 | * 在正常的Map实现中(如HashMap),判断两个键k1和k2相等的条件是:`(k1==null ? k2==null : e1.equals(e2))`;而在IdentityHashMap中,判断两个键值k1和k2相等的条件是`k1==k2`。 153 | 154 | ### Properties 155 | * Properties继承HashTable,表示一个持久的属性集,属性列表中每个key及其value都是一个String。 156 | * Properties被许多Java类使用,例如获取环境变量时它是`System.getProperties()`方法的返回值。 157 | 158 | ## Set接口 159 | Set接口继承Collection接口,存放非重复、无序的元素,使用`equals()`方法比较两个对象是否相同,如果返回true,则两个对象的`hashCode()`返回值也应该相同。 160 | 161 | ### HashSet 162 | * HashSet实现了Set接口,不保证迭代顺序,特别是不保证该顺序恒久不变,允许存放null元素,基本操作的时间复杂度为O(1)。 163 | * HashSet底层使用一个HashMap来保存元素,存入HashSet的元素都存到HashMap的Key中,而value是一个统一值:`private static final Object PRESENT = new Object();`。 164 | * HashSet不是线程安全的,它的迭代器是fail-fast的。 165 | 166 | ### LinkedHashSet 167 | * LinkedHashSet跟LinkedHashMap一样以元素的插入顺序遍历其中的元素,它继承HashSet,所以跟HashSet一样允许存放null元素,也一样不是线程安全的类,基本操作的时间复杂度也是O(1)。 168 | * LinkedHashSet底层使用一个LinkedHashMap来保存元素,存入LinkedHashSet的元素都存到LinkedHashMap的Key中,而value是一个统一值:`private static final Object PRESENT = new Object();`。 169 | 170 | ### TreeSet 171 | * TreeSet是SortedSet接口的唯一实现类,它可以确保集合元素处于排序状态,使用树形结构(算法书中的红黑树)实现,要求其中的元素是可排序的,有两种排序方式: 172 | * 自然排序(Comparable接口),也是默认排序。 173 | * 比较器排序(Comparator接口),构造方法中作为参数传入。 174 | * TreeSet底层使用一个TreeMap来保存元素,存入TreeSet的元素都存到TreeMap的Key中,而value是一个统一值:`private static final Object PRESENT = new Object();`。因此TreeSet跟TreeMap一样,基本操作的时间复杂度为O(log(n)),也一样线程不安全,迭代器是fail-fast的。 175 | 176 | ### EnumSet 177 | * EnumSet是一个与枚举类一起使用的Set,其中所有元素都必须是指定枚举类的枚举值,创建EnumSet时必须显式或隐式地指定它对应的枚举类。 178 | * EnumSet跟EnumMap一样是有序的,根据枚举值在枚举类中的定义顺序来维护Set元素的顺序。 179 | * EnumSet在内部以位向量的形式存储,因此EnumSet对象占用内存很小,而且运行效率很好。 180 | * EnumSet集合不允许加入null元素,如果试图插入null元素将抛出NullPointerException异常。 181 | 182 | ## Best Practice 183 | * 根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList;如果我们不想重复,我们应该使用Set。 184 | * 一些集合类允许指定初始容量,所以如果能够估计好存储元素的数量,就可以提前设置好容器大小,避免重新哈希或大小调整带来的开销。 185 | * 基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。 186 | * 总是使用类型安全的泛型,避免在运行时出现ClassCastException。 187 | * 使用JDK提供的不可变类作为Map的key,可以避免自己实现`hashCode()`和`equals()`。 188 | * 尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。 189 | 190 | -------------------------------------------------------------------------------- /数据库/基础知识.md: -------------------------------------------------------------------------------- 1 | # 基础知识 2 | 3 | ## 数据库范式 4 | 5 | ### 范式级别 6 | 7 | #### 第一范式(1NF) 8 | * 要求:强调每一列的原子性,即每一列都不能再拆分。 9 | * 举例:地址中包含省份名和城市名,如果这两项需要单独处理,则不满足1NF,需要将地址列拆分成两列。 10 | 11 | #### 第二范式(2NF) 12 | * 要求:在满足1NF的基础上,表必须包含主键,并且没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。 13 | * 举例:设计一个订单表(订单编号,下单时间,商品编号,商品名称,商品单价,商品数量),将订单编号和商品编号作为联合主键,这样一来,表中商品名称和商品单价仅仅依赖于商品编号,没有完全依赖主键,违反了2NF,需要拆分为商品表(商品编号,商品名称,商品单价)和订单表(订单编号,下单时间,商品编号,商品数量)。 14 | 15 | #### 第三范式(3NF) 16 | * 要求:在满足2NF的基础上,非主键列必须直接依赖于主键,不能存在传递依赖。 17 | * 举例:设计一个学生表(学号,姓名,年龄,所在学院,学院地点,学院电话),其中学院地点和学院电话依赖于所在学院,所在学院又依赖于学号,构成传递依赖,不符合3NF,需要拆分为学生表(学号,姓名,年龄,所在学院)和学院表(学院名称,学院地点,学院电话)。 18 | 19 | ### 使用范式的好处 20 | * 减少数据冗余。 21 | * 避免插入异常、修改异常和删除异常。 22 | 23 | ## 数据库索引 24 | 索引是在表的列上创建的,用来存储该列的值的一种数据结构,常用的索引包括B树索引、B+树索引和Hash索引。 25 | 26 | ### 索引的实现原理 27 | 包括B树索引、B+树索引和Hash索引。 28 | 29 | #### B树索引 30 | * B树是平衡多路查找树,相对于二叉树,B树每个结点有多个分支,层数相对比较少,可以缩短搜索路径。 31 | * 每个结点都包含了索引字段的键值,成功搜索一个对象可能不需要到达树的叶结点。 32 | * 成功搜索包括沿路径搜索和结点内搜索,成功搜索时间取决于目标结点所在的层次和结点内键值数量。 33 | 34 | #### B+树索引 35 | * B+树是B树的变种,也是一种多叉树,但它的所有叶结点都在同一层。 36 | * 所有非叶结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 37 | * 所有叶结点中包含满足相应查询路径条件约束的全部关键字,按照从小到大的顺序链接。 38 | 39 | #### Hash索引 40 | * 使用Hash表来存储数据,大多使用链表法解决冲突。 41 | * 检索数据的时间复杂度可以达到O(1),取决于冲突的数据量。 42 | * 比较适合等式比较的操作,不能加速排序,也不能加速范围查找,只能使用整个关键字来搜索一行。 43 | 44 | ### B+树比B树更适合用于文件索引和数据库索引的原因 45 | * 由于局部性原理,B+树的结点比B树小,同一盘块中容纳的关键字数量更多,一次性读入内存的关键字更多,可以减少读取磁盘次数。 46 | * B+树所有关键字的查询路径长度基本相同,因此查询效率更稳定。 47 | * B+树范围查询很容易,直接从叶结点挨个儿扫一遍就行,B树还得中序遍历所有结点,效率太低。 48 | 49 | ### 聚集索引和非聚集索引 50 | 51 | #### 聚集索引 52 | * 表中数据根据聚集索引的数据结构来组织。 53 | * 比如原来表的数据是一行一行整齐放在磁盘上,用了B树聚集索引后表的数据就变成树状存放在磁盘上。 54 | 55 | #### 非聚集索引 56 | * 和聚集索引一样采用树或Hash的数据结构来维护,各结点的值为索引字段的值。 57 | * 如果给表中多个字段加上非聚集索引,就会出现多个独立的索引结构,它们相互之间不存在关联。 58 | * 每次给字段新建一个索引,字段中的数据就会被复制一份出来生成索引结构,所以会增大表的体积。 59 | 60 | #### 二者比较 61 | 通过聚集索引可以直接查到需要的数据,而通过非聚集索引需要先查到记录对应的主键值,再根据主键值通过聚集索引查到需要的数据。(参考:[深入浅出数据库索引原理](https://zhuanlan.zhihu.com/p/23624390)) 62 | 63 | #### 主键和聚集索引的区别 64 | 主键是一种唯一索引,它可以是聚集索引,也可以是非聚集索引。大多数数据库创建主键时,都会自动创建主键的聚集索引。 65 | 66 | ### 索引的优点和缺点 67 | 68 | #### 优点 69 | * 通过创建唯一性索引,可以保证表中每一行数据的唯一性。 70 | * 可以大大加快数据的检索速度。 71 | * 可以加速表和表之间的连接。 72 | * 可以减少对检索结果进行分组和排序的时间。 73 | 74 | #### 缺点 75 | * 创建索引需要额外的时间和空间开销。 76 | * 修改表中数据时也要同时维护索引,降低了数据维护速度。 77 | 78 | ### 索引的使用场景 79 | * 在经常需要检索的列上创建索引,可以加快检索的速度。 80 | * 在作为主键的列上创建索引,可以强制该列的唯一性和组织表中数据的结构。 81 | * 在经常用作外键的列上创建索引,可以加快表连接的速度。 82 | * 在经常需要排序的列上创建索引,因为索引已排序,可以加快排序查询时间。 83 | * 在经常需要根据范围搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的。 84 | * 在经常用在where子句中的列上创建索引,可以加快条件判断速度。 85 | 86 | ## 数据库事务 87 | * 一个事务通常包含对数据库或读或写的一个操作序列,提交时可以确保事务中所有操作都依次成功执行,如果其中某个操作不能完成,则该事务中所有操作都要被回滚,即要么全部执行,要不都不执行。 88 | * 多个事务同时执行时,相互之间没有关联,每个事务都能独立运行。 89 | * 为了实现回滚功能,通常需要维护一个事务日志来追踪事务中的操作。 90 | 91 | ### 事务的四大特性:ACID 92 | * 原子性(Atomic):事务作为一个整体被执行,其中的数据库操作要么全部执行,要么都不执行。 93 | * 一致性(Consistency):确保事务执行之后数据库的数据从一个一致状态转变为另一个一致状态,而不可能会出现中间的过程态。 94 | * 隔离性(Isolation):多个事务并发执行时,一个事务的执行不能影响其他事务的执行,提供多种隔离级别。 95 | * 持久性(Durability):已被提交的事务中对数据库的修改应该永久被保存在数据库中。 96 | 97 | ### 事务存在的三个问题 98 | * 脏读:事务A对数据作了修改但还没提交,这时事务B访问并使用了这个数据,因为事务A还没有提交,所以事务B读到的是脏数据。 99 | * 不可重复读:在同一事务中,两次读取同一数据,得到的数据内容不同,这是因为在该数据在两次读取期间被另一个事务修改并提交了。 100 | * 幻读(虚读):在同一事务中,用同一操作两次读取,得到的记录数不同,这是因为在两次读取期间被另一个事务添加或删除了某些记录并提交了。 101 | 102 | ### 事务的四大隔离级别 103 | * 可串行化(Serializable):最高的隔离级别,可以解决幻读问题。读数据加表级锁,事务结束后释放;写数据加表级锁,事务结束后释放。不会发生脏读、不可重复读和幻读。 104 | * 可重复读(Repeatable reads):可以解决不可重复读现象。读数据加行级锁,事务结束后释放;写数据加行级锁,事务结束后释放。可能发生幻读,不会发生脏读和不可重复读。 105 | * 提交读(Read committed):只能读到其他事务已经提交的操作修改的数据。读数据加行级锁,读完该行就释放;写数据加行级锁,事务结束后释放。可能发生不可重复读和幻读,不会发生脏读。 106 | * 未提交读(Read uncommitted):最低的隔离级别,可能读到其他事务中未提交的操作修改的数据。读数据不加锁;写数据加行级锁,写完该行就释放。可能发生脏读、不可重复读和幻读。 107 | 108 | ### JDBC对事务的支持 109 | * JDBC中Connection的auto-commit属性可以指定事务的粒度,默认为true: 110 | * 当auto-commit为true时,每条SQL语句执行完毕后立即自动提交事务; 111 | * 当auto-commit为false时,每个事务都必须显式调用`commit()`方法进行提交,或者显式调用`rollback()`方法进行回滚。 112 | * JDBC中Connection的`setTransactionIsolation()`方法设置事务的隔离级别。 113 | 114 | ```java 115 | try { 116 | conn.setAutoCommit(false); // 将自动提交设置为false 117 | ps.executeUpdate("修改SQL"); // 执行修改操作 118 | ps.executeQuery("查询SQL"); // 执行查询操作 119 | conn.commit(); // 当两个操作成功后手动提交 120 | } catch (Exception e) { 121 | conn.rollback(); // 一旦有操作出错就回滚 122 | } 123 | ``` 124 | 125 | ## 数据库优化(主要针对MySQL) 126 | 127 | ### SQL语句优化 128 | * 优化insert语句,一次插入多行数据。 129 | * 尽量避免在where子句中使用!=或<>操作符,否则引擎将放弃使用索引而改为全表扫描。 130 | * 尽量避免在where子句中对字段进行null值判断,否则引擎将放弃使用索引而改为全表扫描。 131 | * 优化嵌套查询和表连接操作。 132 | * 很多时候用exists代替in是一个好的选择。 133 | 134 | ### 索引的优化 135 | * 索引的使用场景:参考数据库索引部分。 136 | * 索引失效的情形: 137 | * 模糊匹配:以%(表示任意0个或多个字符)开头的like语句。 138 | * or语句前后没有同时使用索引。 139 | * 对于多列联合索引,必须满足最左匹配原则。 140 | 141 | ### 表结构优化 142 | * 选择合适的数据类型: 143 | * 尽可能使用较小的数据类型解决问题,减少占用空间。 144 | * 尽可能使用简单的数据类型,比如MySQL处理int要比varchar容易。 145 | * 尽可能使用not null定义字段,可以提高数据完整性。 146 | * 尽量避免使用text类型,可以使用varchar来替换,非用不可时可以考虑分表。 147 | * 范式优化:一般情况下,表的设计应该遵循三大范式,不过也有需要打破范式的情况。 148 | * 表的垂直拆分: 149 | * 把含有多个列的表拆分成多个表,用于解决表宽度问题。 150 | * 包括几种拆分手段:把不常用的字段单独放到同一个表;把经常使用的字段放到同一个表;把大字段独立放入一个表。 151 | * 好处:拆分后业务清晰,而且聚集索引中每个项可以占用更少空间。 152 | * 表的水平拆分: 153 | * 把数据平分到多个表中存储,用于解决表中数据过多的问题。 154 | * 拆分后的每个表结构一致,平分数据的常用方法有mod和hash法。 155 | * 水平拆分会带来一些问题,包括跨分表的查询、统计等,但也有好处,比如可以降低查询时需要读的数据量,减少索引层数。 156 | * 缓存表: 157 | * 可以用一张缓存表来保存经常访问的数据,每次对数据的请求先在缓存表查,查不到再到整张表中查。 158 | * 相当于cache,常用于一张表的数据很多但其中经常访问的数据很少的情况。 159 | 160 | ## 常考点 161 | 162 | ### 存储过程(Stored Procedure) 163 | * 存储过程是一个事先编译好并存储在数据库的一段SQL语句的代码块,就像一个函数一样实现一些功能,然后再给这个代码块取个名字,以后再用到这个功能的时候直接调用。 164 | * 优点: 165 | * 存储过程只在创建时进行编译,以后每次执行都不需再重新编译,执行效率高。 166 | * 存储过程的代码直接存放于数据库中,通过存储过程名直接调用,减少网络通讯。 167 | * 通过存储过程能够使没有权限的用户间接地存取数据库,从而确保安全性。 168 | * 当SQL语句有变动时,可以只修改数据库中的存储过程而不必修改代码,减少工作量。 169 | * 缺点:调试麻烦,移植性差。 170 | 171 | ### 视图(View) 172 | * 视图是一种虚拟的表,通常是有一个表或者多个表的行或列的子集,具有和物理表相同的功能,可以对视图进行增,删,改,查等操作,但对视图的修改不影响原表。 173 | * 用途: 174 | * 可以只暴露一张表的部分字段给访问者。 175 | * 可以用统一的方式查询来源于不同表的数据。 176 | 177 | ### 游标(Cursor) 178 | * 游标是一种能从含有多条数据的结果集中每次提取一条的机制,可以充当指针,它可以指定结果中的任何位置,然后允许用户对指定位置的数据进行处理。 179 | * 游标提供了逐行操作表中数据的方法,可以一次一行或多行地前进或后退。 180 | 181 | ### 触发器(Trigger) 182 | * 触发器是一种特殊类型的,与表相关联的存储过程,它不由用户直接调用,而是在相应表中数据发生变化时自动调用,常常用于强制业务规则和数据完整性。 183 | * 触发器可以包含复杂的Transaction-SQL语句:将触发器和触发它的语句作为可在触发器内回滚的单个事务对待。 184 | * 触发器的种类: 185 | * 数据操纵语言(DML,Data Manipulation Language)触发器:是指触发器在数据库中发生DML事件时启用,其中DML事件指在表或视图中修改数据的INSERT、UPDATE、DELETE语句。 186 | * 数据定义语言(DDL,Data Definition Language)触发器:是指当服务器或数据库中发生DDL事件时启用,其中DDL事件指在表或索引中的CREATE、ALTER、DROP语句。 187 | * 登陆触发器:是指当用户登录SQL Server,并建立会话时触发。 188 | 189 | ### 约束(Constraint) 190 | * 主键约束(Primary Key):用于设置主键,保证唯一性和非空性。 191 | * 唯一约束(Unique):保证唯一性,可以为空,但只能有一个。 192 | * 默认约束(Default):表示该字段数据的默认值。 193 | * 外键约束(Foreign Key):用于设置外键,建立两表间的连接关系。 194 | * 非空约束(Not Null):用于设置非空约束,该字段不能为空。 195 | 196 | ### 乐观锁和悲观锁 197 | 悲观锁与乐观锁是两种常见的资源并发锁设计思路。 198 | 199 | #### 悲观锁 200 | * 悲观锁是指先获取锁,再进行业务操作,即“悲观”的认为所有的操作均会导致并发安全问题,因此要先确保获取锁成功再进行业务操作。 201 | * 常用`SELECT … FOR UPDATE`操作来实现悲观锁,但需要数据库提供支持,因此不是所有数据库都能用。 202 | * 当数据库执行`SELECT … FOR UPDATE`时,会获取select结果集中每一行的行锁,因此其他并发执行的`SELECT … FOR UPDATE`如果试图选中同一行则会阻塞,需要等待行锁被释放,而这些行锁会在当前事务结束时自动释放,因此必须在事务中使用。 203 | * 这里需要特别注意的是,不同的数据库对`SELECT … FOR UPDATE`的支持有所区别,例如Oracle支持的是`SELECT … FOR UPDATE NO WAIT`,表示如果拿不到锁立刻报错,而不是阻塞等待,但MySQL就没有`NO WAIT`这个选项。 204 | * 另外,MySQL中`SELECT … FOR UPDATE`语句执行时,所有扫描过的行都会被锁上。所以在MySQL中用悲观锁时,务必要确定使用了索引,而不是全表扫描。 205 | 206 | #### 乐观锁 207 | * 乐观锁是指先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过,若未被更新过,则更新成功;否则,失败重试。 208 | * 乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持,一般的做法是在需要锁的数据上增加一个版本号或者时间戳,然后按照如下方式实现: 209 | 210 | ```sql 211 | SELECT data AS old_data, version AS old_version FROM table; 212 | UPDATE SET data = new_data, version = new_version WHERE version = old_version; 213 | if (updated row > 0) { 214 | // 乐观锁获取成功,操作完成 215 | } else { 216 | // 乐观锁获取失败,回滚并重试 217 | } 218 | ``` 219 | 220 | * 乐观锁不一定要在事务中,因为数据库每次执行一条UPDATE语句时会获取该行的互斥写锁,直到这一行被成功更新后才释放。所以可以认为UPDATE操作是一个原子操作,需要排除的只是在SELECT原版本号和UPDATE操作之间出现的并发修改情况,通过版本号或时间戳就可以解决。 221 | 222 | #### 应用场景 223 | * 一般情况下,读多写少更适合用乐观锁,读少写多更适合用悲观锁。 224 | * 乐观锁在不发生取锁失败的情况下,开销比悲观锁小,但是一旦发生失败就需要回滚,开销比较大。因此乐观锁适合用在资源竞争不激烈的场景,而悲观锁适合用在资源竞争激烈的场景。 225 | 226 | ### MVCC机制 227 | * 概念:MVCC,Multiple Version Concurrent Control,多版本并发控制,是一种乐观锁的实现方法,可以实现低代价的无锁并发。 228 | * 实现:为每个事务分配单向增长的版本号,从而为每个事务修改保存一个版本,读操作只读事务开始前的版本号,在事务结束后再次读取版本号,如果一致则认为没有并发操作发生,直接提交事务;如果不一致则认为有并发操作已提交修改,需要回滚事务后重试。 229 | * 好处:读操作不需要阻塞写操作,写操作也不需要阻塞读操作,同时还可以避免脏读和不可重复读。 230 | 231 | ### delete、truncate和drop命令的区别 232 | * delete命令:用来删除表的全部或某些数据行,会触发这个表上所有的delete触发器。执行delete之后,用户需要提交或回滚来执行删除或撤销删除。 233 | * truncate命令:用来删除表中所有数据,这个操作不能回滚,也不会触发这个表上的触发器,比不带where的delete命令删除所有数据更快,且使用事务日志的资源更少。 234 | * drop命令:用来删除整张表和其中的所有数据、索引和权限,这个操作不能回滚,也不会触发这个表上的触发器。 235 | 236 | ### 超键、候选键、主键和外键的区别 237 | * 超键:在关系中能唯一标识元组的属性集称为超键,一个属性或多个属性组合在一起都可以作为一个超键。 238 | * 候选键:最小超键,即元组不能再少了,再少就不是超键了。 239 | * 主键:数据库表中可以唯一标示每个存储对象的超键。一张表只能有一个主键,且主键不能为空值。 240 | * 外键:在表A中存在的表B的主键,称该键为表B在表A中的外键。 241 | 242 | ### 关系型数据库和非关系型数据库的区别 243 | 244 | #### 关系型数据库 245 | * 概念:关系型数据库是指采用了关系模型来组织数据的数据库,即二维表格模型。 246 | * 优点: 247 | * 二维表结构贴近逻辑世界,相对于网状结构、层次结构等其他模型来说更容易理解。 248 | * 通用的SQL语言使得操作关系型数据库十分方便。 249 | * 实体完整性、参照完整性和用户定义的完整性可以减少数据冗余。 250 | * 瓶颈: 251 | * 在高并发读写需求中,磁盘I/O是一个很大的瓶颈。 252 | * 在一张包含海量数据的表中查询效率很低。 253 | * 数据库的表结构难以升级和扩展。 254 | * 典型代表:MySQL和Oracle。 255 | 256 | #### 非关系型数据库NoSQL 257 | * 概念:与关系型数据库相对,非关系型数据库是指不使用关系结构来组织数据、且一般不遵循ACID的数据存储系统。 258 | * 理念:非关系型数据库提出另一种组织数据的方式,比如键值对存储,而且结构不固定,比如每个存储对象可以有不同的属性,可以根据自己的需要添加自定义的键值对,这样就可以不局限于固定的结构,减少查询开销。相对地,由于约束少了,非关系型数据库难以体现设计的完整性。 259 | * 分类: 260 | * 面向高性能读写的key-value数据库:有极高的并发读写性能,主要代表是Redis。 261 | * 面向海量数据访问的文档数据库:可以在海量数据中快速查找数据,主要代表为MongoDB。 262 | * 面向可扩展性的分布式数据库:可以解决传统数据库存在的可扩展性缺陷,可以适应数据量的增加以及数据结构的变化。 263 | 264 | #### 二者使用场景区别 265 | * 事务的一致性:关系型数据库适用于所有对一致性有要求的系统,如银行系统;非关系型数据库适用于对一致性要求不太严格的应用,比如网页应用,两个人看到同一好友的数据更新时间相差几秒是可以容忍的。 266 | * 读写性能:因为维护一致性付出的代价就是读写性能差,像微博这样的应用是无法忍受的,因此需要使用非关系型数据库。 267 | * 可扩展性:关系型数据库具有固定的表结构,可扩展性很差,难以应付系统升级、功能增加而带来的数据结构变动,因此需要使用非关系型数据库。 268 | 269 | ### 常见的聚合函数 270 | * `count(*)`:统计表中所有记录的个数。 271 | * `count(列名)`:统计一列中所有值的个数,重复值也会被当作有效的记录。 272 | * `count(distinct 列名)`:统计一列中所有值的个数,其中重复的记录只会被记录一次。 273 | * `sum(列名)`:计算一列值的总和。 274 | * `avg(列名)`:计算一列值的平均值。 275 | * `max(列名)`:找出一列值中的最大值。 276 | * `min(列名)`:找出一列值中的最小值。 277 | -------------------------------------------------------------------------------- /基于多核的并行编程/Ch1-1 并行计算模型与系统.md: -------------------------------------------------------------------------------- 1 | # Ch1-1 并行计算模型与系统 2 | 3 | ## Flynn分类法 4 | 5 | ### SISD(Single Instruction,Single Data stream) 6 | ![SISD](media/并行计算模型与系统/SISD.jpg) 7 | 8 | * 所有指令串行执行,且在某个时钟周期内CPU只能处理一个数据流,处理速度由处理器决定。 9 | * 应用:单处理器的计算机、冯诺依曼结构。 10 | 11 | ### SIMD(Single Instruction,Multiple Data Streams) 12 | ![SIMD](media/并行计算模型与系统/SIMD.jpg) 13 | 14 | * 同一时刻,各个处理器做的指令完全一致,但接受的数据不同。 15 | * 应用:CRAY向量机、Intel MMX多媒体支持/流媒体加速。 16 | 17 | ### MISD(Multiple Instructions,Single Data Stream) 18 | * 对同一数据采用不同指令来处理。 19 | * 仅为理论模型,实际一般不采用。 20 | 21 | ### MIMD(Multiple Instructions,Multiple Data Streams) 22 | ![MIMD](media/并行计算模型与系统/MIMD.jpg) 23 | 24 | * 不同处理器接受不同的数据集,对其采用不同的指令处理,是应用最广泛的模型。 25 | * 应用:Intel和AMD的双核处理器等。 26 | * 按照不同的指标,还可以继续划分。 27 | 28 | ## MIMD的子分类 29 | 30 | ### 共享内存(Shared Memory) 31 | * 特点:所有处理器共享一块内存区域。 32 | * 优点:比分布式存储简单,CPU之间不需要直接通信。 33 | * 缺点:有局限性,一个处理器会影响整个系统;可靠性和可扩展性差,处理器增加会导致内存争存,内存带宽是最大问题。 34 | 35 | ### 分布式存储(Distributed Memory) 36 | * 特点:每个处理器有自己私有的内存空间,通过互联网络(IPC channel)通信。 37 | * 优点:可扩展性强。 38 | * 缺点:比共享存储复杂,实现难度更大。 39 | 40 | ## MIMD的另一种分类 41 | * SPMD(Single Program,Multiple Data):不同处理器只能执行同一个程序。 42 | * MPMD(Multiple Program Multiple Data):不同处理器可以执行多个不同的程序。 43 | 44 | ## 多节点系统架构 45 | ![多节点系统架构](media/并行计算模型与系统/多节点系统架构.jpg) 46 | > 注:实际的机器一定不会将缓存和寄存器共享,因为缓存都是针对特定处理器的。 47 | 48 | ## 并行计算机模型 49 | 50 | ### 特点 51 | * 主要是理论模型,用于设计并行算法。 52 | * 来自程序员视角的抽象并行机,不同于冯诺依曼架构中的顺序执行。 53 | 54 | ### 定义:语义属性和性能属性 55 | 56 | #### 语义属性 57 | * 同构性:执行并行程序时,并行计算机中处理器的行为相似到何种程度以及该模型如何分类到Flynn标准中。 58 | * 同步性:处理器执行指令的同步有多严格。 59 | * 交互机制:处理器的执行线索(并行进程或线程)之间如何相互影响,如何获知对方的数据。 60 | * 地址空间:进程或线程可访问的地址空间。 61 | * 单地址空间和多地址空间:多地址空间时,两个CPU的内存空间并不完全重合,同一地址可能对应不同的物理单元。 62 | * 均匀存储器访问(UMA,Uniform Memory Access)和非均匀存储器访问(NUMA,Non-Uniform Memory Access):访问一段存储器内容不同单元的速度相同或不同,虚存、远程存储等因素都可能造成此现象。 63 | * 存储器模型:如何处理共享存储器的访问冲突。 64 | 65 | #### 性能属性 66 | 67 | | 名称 | 说明 | 符号 | 单位 | 68 | | --- | --- | --- | --- | 69 | | 机器规模 | 可以并行执行的计算单元数量 | n | 无量纲 | 70 | | 时钟速率 | CPU的主频 | f | MHz | 71 | | 工作负载 | 程序运行完成需要的指令数量,不同的指令类型执行时间不同,这里统一以浮点指令为衡量标准 | W | Mflop | 72 | | 顺序执行时间 | 串行程序运行时间 | T1 | s | 73 | | 并行执行时间 | 并行程序运行时间 | Tn | s | 74 | | 速度 | 单位时间执行的工作量,Pn=W/Tn | Pn | Mflop/s | 75 | | 加速比 | Sn=T1/Tn | Sn | 无量纲 | 76 | | 效率 | 单位核心上的加速比,En=Sn/n | En | 无量纲 | 77 | | 利用率 | 并行计算的速度与多台机器并行计算的速度和的比值,Un=Pn/(n*Ppeak) | Un | 无量纲 | 78 | | 启动时间 | | t | us | 79 | | 渐进带宽 | 描述网络在长期传输时能达到的稳定速率 | r | MB/s | 80 | 81 | ## 并行系统中的操作和开销 82 | 83 | ### 并行系统中的操作类型 84 | * 计算操作:原有串行计算机中的所有操作类型,包括传统的序列编程里有的算数逻辑、数据转换、控制流等操作。 85 | * 并行操作:管理进程/线程的操作,包括创建/终止、分组和上下文切换。 86 | * 交互操作:进程/线程的通信、同步操作。 87 | 88 | ### 并行系统的开销(overhead) 89 | * 并行开销:由进程/线程管理而产生的开销。 90 | * 通信开销:由进程/线程间交换信息而产生的开销。 91 | * 同步开销:由进程/线程的同步操作而产生的开销。 92 | * 负载不平衡开销:因各个计算单元的负载分配不平衡而产生的开销。如果负载的平均的,各计算单元都能满负荷运行,任务量大小的分别造成了执行时间的加长。 93 | 94 | ### 不可并行的例子 95 | * Ackerman函数:一般递归问题,必须要求将返回值表之前的返回值全部计算出来。 96 | * 加密算法:设计者特意如此,一旦加密算法可并行,密码的破解会相当容易。 97 | * 无理数逼近:通过迭代法进行,没有之前的结果不可进行后一次迭代。 98 | * 工作流、控制流。 99 | 100 | ## 并行计算模型 101 | 102 | ### PRAM模型(Parallel Random Access Model) 103 | 104 | #### 特点 * 规模为n的PRAM:n个处理器,一个共享空间,一个公共时钟。 105 | * 机器规模n可以为任意大,基本时间步称为周期,在一个周期内每个CPU只能执行一条指令,这条指令有可能是空指令(不做任何操作),如果是空指令则称处理器在该周期闲置。 106 | * 一个周期中可以用一条指令完成读存储器、算术操作、写存储器三个操作。 107 | * 所有处理器共享一个公共时钟(隐式同步),所以不需要同步开销;CPU之间通过读、写共享变量进行通信,所以也不需要通信开销;PRAM中没有启动和关闭进程/线程的问题,所以也没有并行开销。因此PRAM只需要考虑负载不均衡开销。 108 | * 根据是否可以同时读写,PRAM又分为以下三类: 109 | * PRAM-EREW:Exclusive Read,Exclusive Write,互斥读写。 110 | * PRAM-CREW:Concurrent Read,Exclusive Write,并发读互斥写。 111 | * PRAM-CRCW:Concurrent Read,Concurrent Write,并发读写。 112 | 113 | #### 语义属性 114 | * 同构性:n=1时是SISD;n>1时是MIMD。 115 | * 同步性:指令级同步,与实际的MIMD不同。 116 | * 交互机制:通过共享变量(或共享存储器)进行交互 117 | * 地址空间:单地址空间,均匀存储器访问。 118 | * 存储器模型:EREW、CREW、CRCW等。 119 | 120 | #### 优缺点 121 | * 优点:简单,多数的理论并行算法均采用PRAM模型或其变异加以描述,也被广泛用来分析并行算法复杂度。 122 | * 缺点:对于无通信开销、指令级同步的假设不太现实。 123 | 124 | #### 例题 125 | * 问题:在n个处理器的PRAM-EREW上,求两个N维向量A、B的内积,当N很大时,加速比为多少?(假设乘法和加法各占一个时钟周期) 126 | * 串行:乘法一共N次,加法一共N-1次,因此串行需要的周期数为2N-1。 127 | * 并行:并行计算时,乘法可以完全平均分配,加法除了最后几部分,前面的都可以平均分配,因此一共需要(2N-1-(n-1))/n+logn个周期,分子的n-1是剩下的可以平均分配的加法操作。 128 | * 加速比:(2N-1)/((2N-1-(n-1))/n+logn) 129 | * 注意,当N>>n时,加速比近似为n。 130 | ### APRAM模型(Asynchronous PRAM):异步的PRAM 131 | 132 | #### 特点 * 克服PRAM模型的缺点,保留其简单性。 133 | * 比PRAM模型更接近实际的并行计算机。 134 | * APRAM模型中,计算由一系列用同步路障(Synchronization Barrier)分开的全局Phase(称为相或阶段)组成。所以APRAM也称Phase PRAM。 135 | * 每个处理器有自己的局部存储器、局部时钟、局部程序。 136 | * 处理器间通信通过共享存储器完成。 137 | * 没有全局时钟,各处理器独立的异步的执行各自操作。 138 | * 处理器任何时间依赖关系需明确地在各处理器的程序中加入同步路障。 139 | * 一条指令可在非确定但有限的时间内完成。 140 | 141 | #### 四类指令 * 全局读:将全局存储器单元中的内容读入本地存储器单元中。 142 | * 全局写:将本地存储器单元中的内容写入全局存储器单元中。 143 | * 局部操作:对本地存储器中的数执行操作,其结果存入本地存储器中。 144 | * 同步:同步是计算中的一个逻辑点,在该点各处理器均需等待别的处理器到达后才能继续执行其局部程序。 ### BSP模型(Bulk Synchronization Parallel) 145 | 146 | #### 特点 147 | * 克服PRAM模型的缺点,保留其简单性。 148 | * 一个BSP计算机由n个处理器-存储器对(节点)组成,它们之间借助通信网络进行互连。 149 | * 分布式存储的MIMD模型。 150 | * BSP模型中,计算由一系列由同步路障分开的超步(superstep)组成,因此BSP是超步级的松同步。 151 | * 超步 152 | * 由计算操作、通信操作、同步操作组成。 153 | * 假定局部操作可在一定时间内完成,而在每一超步中,一个处理器至多发送或接受h条消息(h-relation),则执行一个超步的最大时间:W+gh+L,其中W是指每个超步内的最大计算时间,L是指路障同步开销,g是指发送每条消息的开销。 #### 例题 154 | * 内积求解问题回顾:在n个处理器的BSP模型上,求两个N维向量A、B的内积。 155 | * 并行执行时间为:(2N-1-(n-1))/n+(g+L+1)logn 156 | 157 | ## 内存访问模型 158 | 159 | ### 多级存储体系结构 160 | * 为了解决内存墙(memory wall)的性能瓶颈问题。 161 | * 在节点内部的cache称为二级cache(L2 cache),在处理器内部更小的cache称为一级cache(L1 cache),一级cache连接CPU寄存器和二级cache,负责缓存二级cache中的数据到寄存器中。 162 | 163 | ![多级存储体系结构](media/并行计算模型与系统/多级存储体系结构.jpg) 164 | 165 | ### cache的映射策略:内存块和cache线之间的映射关系 166 | * 直接映射(Direct Mapping Strategy):每个内存块只能被唯一映射到指定的一条cache line中。 167 | * 全关联映射(Full Association Mapping Strategy):内存块可以被映射到任意一条cache line中。 168 | * N-路组关联映射(N-Way Set Association Mapping Strategy):cache被分解为V个组,每个组由N条cache line组成,内存块按直接映射策略映射到某个组,但在该组中,内存块可以被映射到任意一条cache线。 169 | 170 | ## 并行计算机访存模型 171 | 172 | ### UMA(Uniform Memory Access)模型 173 | * 物理存储器被所有节点共享。 174 | * 所有节点访问任意存储单元的时间相同。 175 | * 发生访存竞争时,仲裁策略对每个节点平等对待。 176 | * 各节点的CPU可带有局部私有高速缓存。 177 | * 外围I/O设备也可以共享,且每个节点有平等的访问权利。 178 | 179 | ### NUMA(Non-Uniform Memory Access)模型 180 | * 物理存储器被所有节点共享,任意节点可以直接访问任意内存模块。 181 | * 节点访问内存模块的速度不同,访问本地存储模块的速度一般是访问其它节点内存模块的3倍以上,但3倍以下的速度差异一般也近似认为是UMA。 182 | * 发生访存竞争时,仲裁策略对节点是不等价的,存在优先级的差异。 183 | * 各节点的CPU可带有局部私有高速缓存。 184 | * 外围I/O设备也可以共享,但对各节点是不等价的。 185 | 186 | ### COMA(Cache-Only Memory Access)模型 187 | * 各处理器节点中没有存储层次结构,全部高速缓存组成了全局地址空间。 188 | * 利用分布的高速缓存目录D进行远程高速缓存的访问。 189 | * COMA中的高速缓存容量一般都大于二级cache容量。 190 | * 使用COMA时,数据开始时可以任意分配,因为在运行时最终会被迁移到要用到它的地方。 191 | * COMA的本质是一种特殊的NUMA。 192 | 193 | ### NORMA(No-Remote Memory Access)模型 194 | * 所有存储器都是私有的。 195 | * 绝大多数NORMA都不支持直接访问远程存储器。 196 | * 在DSM中,NORMA就消失了。 197 | 198 | ### CC-NUMA(Cache-Coherent Non-Uniform Memory Access)模型 199 | 200 | #### 缓存一致性 201 | * 相同信息项在不同层次存储器中拷贝保持一致,如果某个存储块被修改,其它层次的缓存块就必须被更新。 202 | * 单处理器下的缓存一致性策略 203 | * 写直达法(WT,Write-Through):每次写入cache时也同时写入内存,通常需要专用策略。 204 | * 写回法(WB,Write-Back):每次存储块从cache中被换出时,将其写入内存。 205 | 206 | #### 监听一致性协议 207 | * 写无效协议:在本地cache被修改后,使所有其它位置的数据拷贝失效。在没有专用电路时,写无效协议的效率更高。 208 | * 写更新协议:在本地cache被修改后,广播修改的数据,使得其它位置的数据拷贝得以及时更新。 209 | 210 | #### CC-NUMA的特点 211 | * 保证缓存一致性的NUMA。 212 | * 大多数使用基于目录的高速缓存一致性协议,目录保留着内存块的共享信息。 213 | * 保留SMP结构易于编程的优点,也改善常规SMP的可扩展性: 214 | * 写程序不需要关心内存读/写的来源,一旦计算能力不够,可以通过增加节点的方式实现。 215 | * CC-NUMA系统中各节点间不用总线连接,因此可以包含更多节点,解决了SMP不可扩展的问题。 216 | * 同时,CC-NUMA仍是单一地址空间,所有节点中的存储器统一编址,构成一个统一的地址空间,该地址空间被所有处理器共享,保持了SMP系统易于编程的特点。 217 | * 在CC-NUMA中,由于数据空间局部性的原因,处理器大多数数据访问都可限制在本地节点内,网络上的通信主要使用高速缓存一致性命令,而不是传输数据。 218 | * CC-NUMA实际上是一个分布共享存储的DSM多处理机系统,这里的共享存储是软件层面的东西,通过系统封装来实现。 219 | 220 | #### CC-NUMA的优点 221 | 程序员无需明确地在节点上分配数据,系统的硬件和软件开始时自动地在各节点分配数据:在运行期间,高速缓存一致性硬件会自动地将数据迁移到需要用到它的地方。 222 | 223 | ## 内存访问模型分类 224 | ![内存访问模型分类](media/并行计算模型与系统/内存访问模型分类.jpg) 225 | 226 | ## 典型并行计算模型 227 | 228 | ### SMP(Symmetric Multi-Processor) 229 | 230 | #### 特点 231 | * 各个处理器一模一样。 232 | * 处理器在总线上的接口是对称的,接到同一条总线上。 233 | * 所有内存都是共享内存。 234 | * 采用均匀存储访问。 235 | * 现行多核PC的架构即为SMP。 236 | 237 | #### 结构图 238 | ![SMP结构图](media/并行计算模型与系统/SMP结构图.jpg) 239 | 240 | #### 优点 241 | * 结构对称,使用单一操作系统 242 | * 所有处理器通过个高速总线或交叉开关与共享存储器相连,具有单一的地址空间 243 | * 通过读/写共享变量完成通信,快捷且编程比较容易。 244 | 245 | #### 缺点 246 | * 存储器和I/O负载大,易成为系统的瓶颈,限制了系统中处理器的数量。 247 | * 总线会成为整个SMP的瓶颈,CPU通常在32个以内,CPU和内存都不能无限制地增加。 248 | * 内存由所有CPU共享,CPU越多,内存冲突的概率就越大;要解决这些冲突,要么加锁影响性能,要么放任导致错误。 249 | * 单点失效会导致整个系统的崩溃,因为各处理器的设计是对称的,一旦处理器损坏,对称性就失效了。 250 | * 一次成型,扩展性差,不能随意增加和减少CPU数量。 251 | 252 | #### 典型系统 253 | * SGI Power Challenge L 2-6CPU 254 | * SGI Power Challenge L 2-18CPU 255 | 256 | ### PVP(Parallel Vector Processors) 257 | 258 | #### 特点 259 | * PVP是并行向量处理机,对应于SIMD的结构。 260 | * PVP含有为数不多、功能很强的定制向量处理器(VP),每个处理器性能至少为1Gflop/s。 261 | * 定制的高带宽从横交叉开关网络将这些向量处理器连到若干共享存储器(SM)模块,每个模块提供高速数据访问。 262 | * 这类机器通常不使用cache,而是使用大量向量寄存器以及指令缓存。 263 | * GPU仍偏好使用此类VP处理器。 264 | 265 | #### 结构图 266 | ![PVP结构图](media/并行计算模型与系统/PVP结构图.jpg) 267 | 268 | #### 机器实例 269 | * Cray C-90 270 | * Cray T-90 271 | * NEC SX-4 272 | 273 | ### MPP(Massive Parallel Processor) 274 | 275 | #### 简介 276 | * 由上千台处理机组成,峰值可达3T性能目标: 277 | * Tflops计算能力 278 | * TB主存容量 279 | * TB/s的I/O带宽 280 | * 用来解决“重大挑战性”问题,是国家综合国力的体现。 281 | * 开发困难,价格高,市场有限。 282 | * 每个节点有自己的处理器与内存,通过定制网络将这些节点相连,因而存储是分布式的。 283 | * 网络接口直接连接到节点的存储总线上,紧耦合。 284 | 285 | #### 结构图 286 | ![MPP结构图](media/并行计算模型与系统/MPP结构图.jpg) 287 | 288 | #### 特点 289 | * 专门设计制造高速互联网络。 290 | * 节点内有一个或多个处理器、高速缓存、一个本地存储器和互连网络。 291 | * 每个处理器只能直接访问本地存储器,而不能直接访问其它处理器的存储器。 292 | * 程序由多个进程组成,进程间采用消息传递机制。 293 | * 使用专用硬件提升性能,技术复杂,成本高。 294 | 295 | #### 典型系统 296 | * Cray T3D 297 | 298 | ### Cluster(或COW) 299 | 300 | #### 特点 301 | * 与MPP不同,直接使用商用网络与商用硬件,但成本远低于MPP。 302 | * 每个节点都是一个完整的计算机,各节点都有本地磁盘和完整的操作系统(各节点的操作系统可以不同),且各节点通过低成本的商用网络互连。 303 | * Cluster的节点与系统级网络的网络接口连接到I/O总线上(松耦合),而MPP的网络接口连接到存储总线上(紧耦合)。 304 | 305 | #### 结构图 306 | ![Cluster结构图](media/并行计算模型与系统/Cluster结构图.jpg) 307 | 308 | #### 优点 309 | * 高性价比: 310 | * 硬件方面:主要使用商用计算机硬件,造价低,配置灵活。 311 | * 软件方面:大量使用开源软件系统。 312 | * 能够以更低的价格获得更高的峰值计算速度。 313 | * 良好的可扩展性:可以根据需要灵活调整集群系统的规模。 314 | * 易获得性,配置灵活: 315 | * 相对于MPP,设计建造技术要求降低。 316 | * 可以用有限的资金获得高峰值速度。 317 | * 突破SMP和MPP的技术限制。 318 | * 可以根据实际情况挑选集群系统的各种组件。 319 | * 提供多用途的并行计算系统:科学计算、商业应用、互联网应用等等,已经成为主流的企业计算系统。 320 | 321 | #### 实例 322 | * Digital TruCluster 323 | * IBM SP2 324 | * Berkeley NOW 325 | 326 | -------------------------------------------------------------------------------- /基于多核的并行编程/Ch2-3 OpenMP多核编程.md: -------------------------------------------------------------------------------- 1 | # Ch2-3 OpenMP多核编程 2 | 3 | ## OpenMP的介绍 4 | 5 | ### 概览 6 | * 提供线程级别的基于共享内存的并行模型。 7 | * 多线程实现的物理结构是SMP。 8 | * 本身只是提供一种规范,具体的实现由各个系统和编译器负责实现。 9 | 10 | ### OpenMP的实现层次 11 | * 编译指导(Compiler Directives) 12 | * 运行时库函数(Runtime Library Routines) 13 | * 环境变量(Environment Variables) 14 | 15 | ### OpenMP的目标 16 | * 标准化:在不同的语言和架构上都可以以相同的方式编写多核程序。 17 | * 简洁有效:编译器的指导语句尽可能地少。 18 | * 易用性:允许程序逐步并行化,使对串行程序的修改尽可能地少。 19 | * 可移植性:多种语言、不同平台。 20 | 21 | ### OpenMP的编程模型 22 | * 共享内存、基于线程的并行模型 23 | * 显式并行 24 | * Fork-Join模型:程序启动后是单线程,达到需要并行的部分(并行区)时,产生多个线程同时运行,所有线程同时执行完后互相等待,一起结束。 25 | * 编译指导(Compiler Directive Based) 26 | * 嵌套并行(Nested Parallelism Support) 27 | * 动态线程的创建与销毁:线程的数量可以由OpenMP自适应。 28 | * I/O:OpenMP并没有指定I/O的接口,仍然按原有的方式进行读写,因此并行区中的读写会面临冲突的问题,需要程序员自己解决。 29 | * 内存模型 30 | 31 | ### 编程注意事项 32 | * 编译器命令选项: 33 | * gcc:`-fopenmp` 34 | * pgi:`-mp` 35 | * intel:`/Qopenmp` 36 | * VS:`/openmp`,或直接在项目属性中添加OpenMP支持 37 | * `#include ` 38 | 39 | ## 创建线程 40 | 41 | ### Fork-Join模型 42 | * 主线程根据需要创建一组线程执行并行任务,创建后就进入了并行区。 43 | * 在并行区中,主线程作为其中一个线程。 44 | * 并行区可以嵌套,并且在子并行区中,仍有相应概念上的主线程。 45 | 46 | ![Fork-Join模型](media/OpenMP多核编程/Fork-Join模型.jpg) 47 | 48 | ### 编译指导语句:创建线程,进入临界区 49 | #pragma omp parallel 50 | { 51 | 52 | } 53 | 54 | ### 指定线程个数 55 | // 库函数:此函数之后的每个并行区都是4个线程同时运行。 56 | omp_set_num_threads(4); 57 | 58 | // 编译指导语句:只对一个并行区生效。 59 | #pragma omp parallel num_threads(4) 60 | { 61 | // 并行区 62 | } 63 | 64 | * 如果不指定线程个数,默认线程个数为当前处理器数或处理器核心数。 65 | 66 | ## 线程同步(Synchronization) 67 | 68 | ### 高层同步 69 | 70 | #### 临界区(critical):同一时刻只有一个线程能进入临界区 71 | float res; 72 | #pragma omp parallel 73 | { 74 | float B; 75 | int i, id, nthrds; 76 | id = omp_get_thread_num(); // 当前线程的ID 77 | nthrds = omp_get_num_threads(); // 当前的线程个数 78 | for (i=id; i 161 | omp_lock_t lock; 162 | omp_init_lock(&lck); 163 | 164 | #pragma omp parallel private(tmp, id) 165 | { 166 | id = omp_get_thread_num(); 167 | tmp = do_lots_of_work(id); 168 | omp_set_lock(&lock); 169 | omp_unset_lock(&lock); 170 | } 171 | omp_destroy_lock(&lock); 172 | 173 | ## 并行循环(Parallel Loop) 174 | 175 | ### SPMD与worksharing 176 | * SPMD:Single Program Multiple Data,多个线程基于不同的数据集执行相同代码。 177 | * worksharing:将代码定义的任务分成不同部分,交给多个线程来完成。 178 | 179 | ### worksharing的种类 180 | 181 | #### 循环结构(for循环) 182 | #pragma omp parallel 183 | { 184 | // for的编译指导语句必须写在临界区内 185 | #pragma omp for 186 | for (i=0; i线程数时,先用任务把线程占满,执行完的线程再被分配剩下的任务。 244 | * 任务数<线程数时,其它线程等待。 245 | * section结构出口处有一个隐式路障同步,可以用nowait来关闭。 246 | 247 | #### task(3.0或更高版本) 248 | 249 | ### worksharing的特点 250 | * worksharing结构不创建线程,只把任务分配给创建好的线程。 251 | * worksharing结构的入口没有路障同步,但出口有隐式路障同步。 252 | * 待分配的任务无法执行一部分,要么整个分配,要么不分配。 253 | * 分配时有固定的次序,不支持自定义的次序,也不会随机分配。 254 | 255 | ### 消除循环的数据依赖 256 | ![](media/OpenMP多核编程/消除循环的数据依赖.jpg) 257 | 258 | ### 规约(Reduction) 259 | 260 | #### 基本格式 261 | double ave = 0.0, A[MAX]; 262 | int i; 263 | #pragma omp parallel for reduction (+:ave) 264 | for (i=0; iW、S->R、R->S、W->S、S->S。 362 | * 在这里S代表Synchronization,相关操作为flush。 363 | 364 | ## OpenMP 3.0与Tasks 365 | 366 | ### OpenMP 3.0的特点 * 提供task指令:可以动态分配任务给线程。 * 提供对嵌套并行更好的支持。 * 轻便的线程控制:增加环境变量以控制子线程栈大小和在运行时处理空闲线程。 367 | 368 | ### task指令 369 | 370 | #### 特点 * task = 需要执行的代码 + 独立的数据环境 + 分配的线程 371 | * task包括打包任务和把任务分配给空闲的线程执行,这个分配任务由master线程完成。 372 | 373 | #### 用法 374 | #pragma omp task [clause[[,],clause] ...] 375 | 376 | * clause参数可以为: 377 | * `if (expression)`:当expression为`true`时才执行。 378 | * `untied`:创建的任务默认将会与某个线程绑定,即只能由该线程来完成,但`untied`可以用来解除这样的绑定,它允许任务的创建和分配交给其他线程而不是master线程来做。 379 | * `shared (list)` 380 | * `private (list)` 381 | * `firstprivate (list)` 382 | * `default (shared|none)` 383 | 384 | #### 举例 385 | #pragma omp parallel 386 | { 387 | #pragma omp single private(p) 388 | // 由一个线程进行预处理,其它线程什么都不做。 389 | { 390 | p = listhead; 391 | while (p) { 392 | #pragma omp task 393 | process(p); 394 | p = next(p); 395 | } 396 | } 397 | } 398 | 399 | ### 新引入的库函数和环境变量 400 | ```C++ 401 | omp_get_active_level(); // 获取当前嵌套区的层数 omp_get_ancestor (int); // 获取对应参数层数的嵌套区的父线程ID 402 | omp_get_teamsize (int); // 获取对应参数层数的嵌套区的线程数 403 | 404 | OMP_STACKSIZE // 环境变量:子线程的栈大小 405 | OMP_WAIT_POLICY // 环境变量:线程遇到锁/路障时的等待策略,有两个选择,一个是ACTIVE(先自旋一段时间),另一个是PASSIVE(直接阻塞)。 406 | 407 | OMP_MAX_ACTIVE_LEVELS // 环境变量:嵌套并行区的最大层数 408 | omp_set_max_active_levels (int); // 设置嵌套并行区的最大层数 409 | omp_get_max_active_levels (); // 获取嵌套并行区的最大层数 410 | 411 | OMP_THREAD_LIMIT // 环境变量:最大可用线程数 412 | omp_get_thread_limit(); // 获取最大可用线程数 413 | ``` 414 | 415 | 416 | 417 | 418 | 419 | -------------------------------------------------------------------------------- /数据结构/树(Tree).md: -------------------------------------------------------------------------------- 1 | # 树(Tree) 2 | 树是无向、联通的无环图。 3 | 4 | ## 常见种类 5 | 6 | ### 二叉树(Binary Tree) 7 | * 二叉树是一个树形数据结构,每个结点最多可以有两个子结点,称为左子结点和右子结点。 8 | * 满二叉树:二叉树中的每个结点有0个或2个子结点。 9 | * 完美二叉树:二叉树中的每个结点有两个子结点,并且所有的叶子结点的深度是一样的。 10 | * 完全二叉树:二叉树中除最后一层外其他各层的结点数均达到最大值,最后一层的结点都连续集中在最左边。 11 | * 性质: 12 | * 非空二叉树的第K层上至多有`2^(K-1)`个元素。 13 | * 深度为H的二叉树至多有`2^(H-1)`个结点。 14 | * 存储结构: 15 | * 链式存储:定义结点类,每个结点有自身的值和指向左右子树的指针引用。 16 | * 顺序存储(存在数组中):设一个结点的下标为`i`,则其父结点的下标为`i/2`,其左子结点的下标为`2*i`,其右子结点的下标为`2*i+1`。 17 | 18 | ### 二叉查找/排序树(Binary Search/Sort Tree,BST) 19 | * 二叉查找树(BST)是一种二叉树。其任何结点的值都大于等于左子树中的值,小于等于右子树中的值。 20 | * 时间复杂度: 21 | * 索引:O(log(n)) 22 | * 查找:O(log(n)) 23 | * 插入:O(log(n)) 24 | * 删除:O(log(n)) 25 | 26 | ### 哈夫曼树(Huffman Tree) 27 | * 哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。 28 | * 所谓树的带权路径长度,就是树中所有的叶结点的权值乘其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度则为叶结点的层数)。 29 | * 树的带权路径长度记为`WPL = (W1\*L1 + W2\*L2 + W3\*L3 + … + Wn\*Ln)`,n个权值`Wi(i=1,2,...,n)`构成一棵有n个叶结点的二叉树,相应的叶结点的路径长度为`Li(i=1,2,...,n)`。 30 | * 哈夫曼编码步骤: 31 | 1. 对给定的n个权值{W1,W2,W3,...,Wi,...,Wn}构成n棵二叉树的初始集合F={T1,T2,T3,...,Ti,...,Tn},其中每棵二叉树Ti中只有一个权值为Wi的根结点,它的左右子树均为空。为方便在计算机上实现算法,一般还要求以Ti的权值Wi的升序排列。 32 | 2. 在F中选取两棵根结点权值最小的树作为新构造的二叉树的左右子树,新二叉树的根结点的权值为其左右子树的根结点的权值之和。 33 | 3. 从F中删除这两棵树,并把这棵新的二叉树同样以升序排列加入到集合F中。 34 | 4. 重复以上两步,直到集合F中只有一棵二叉树为止。 35 | 36 | ### 字典树(Trie) 37 | * 字典树,是一种用于存储键值为字符串的动态集合或关联数组的查找树。 38 | * 树中的结点并不直接存储关联键值,而是该结点在树中的位置决定了其键值。 39 | * 根结点是空字符串。 40 | * 示例图:![字典树](media/树/字典树.jpg) 41 | 42 | ### AVL树 43 | * AVL树是一棵平衡的二叉树,其中每个节点的平衡因子是它的右子树的高度减去它的左子树的高度,只有所有结点的平衡因子都等于1、0或-1时,该树才是一个AVL树。 44 | * AVL树可以避免BST退化成近似链的结构,降低树的高度,从而提高查询效率。 45 | * 插入结点时如果失去平衡则需要重新平衡,有4种旋转方式来平衡:单向右旋RR;单向左旋LL;双向旋转(先左后右)LR;双向旋转(先右后左)RL。 46 | 47 | ### 红黑树 48 | * 红黑树,全称是Red-Black Tree,简称R-B Tree,是一种特殊的二叉查找树,它的每个节点上都有自己的颜色,可以是红色或黑色。 49 | * 红黑树的特性: 50 | * 每个节点是黑色或红色的; 51 | * 根节点是黑色; 52 | * 每个叶子节点是黑色,这里叶子节点是指为空的叶子节点; 53 | * 如果一个节点是红色的,则它的子节点必须是黑色的; 54 | * 从根节点到叶子结点的所有路径上都必须包含相同数目的黑色节点。 55 | * 插入新结点时,新结点一定是红色的;如果插入结点后不满足红黑树的条件,则需要经过左旋、右旋和变色等操作来使之重新变为一棵红黑树。 56 | * 示例图:![红黑树](media/树/红黑树.jpg) 57 | 58 | ### B树(Balanced Tree) 59 | * B树是对BST的一种扩展,它的每个结点最多拥有m个子结点且m>=2,空树除外(注:m阶代表一个树结点最多有多少个查找路径,m阶=m路,m=2时是二叉树)。 60 | * 除根结点外,每个结点的关键字数量大于等于`ceil(m/2)-1`个小于等于`m-1`个,非根结点关键字数必须>=2。 61 | * 如果一个非叶结点有N个子结点,则该结点的关键字数等于N-1,并且所有结点的关键字都是按增序排列。 62 | * 两种操作: 63 | * 插入:如果大于`m-1`就要分裂,中间位置的关键字上移。 64 | * 删除:如果小于`ceil(m/2)-1`就要从隔壁借或者合并。 65 | * 示例图:![B树](media/树/B树.png) 66 | * 插入顺序示例图:![B树插入](media/树/B树插入.gif) 67 | 68 | ### B+树 69 | * B+树是B树的一种变形树,它与B树的差异在于: 70 | * 有k个子结点的结点必然有k个关键码; 71 | * 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中; 72 | * 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。 73 | * B+树的优点在于: 74 | * 由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key,数据存放的更加紧密,具有更好的空间局部性,所以访问叶子节点上关联的数据也具有更好的缓存命中率。 75 | * B+树的叶子结点都是相连的,所以对整棵树的遍历只需要遍历叶子结点即可。而且由于数据按顺序排列,便于区间查找和搜索。而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。 76 | * 示例图:![B+树](media/树/B+树.png) 77 | * 插入顺序示例图:![B+树插入](media/树/B+树插入.gif) 78 | 79 | ## 常见应用 80 | 81 | ### 二叉树的遍历 82 | 83 | #### 前序遍历:根->左->右(DFS) 84 | ```java 85 | List preorder(Node root) { 86 | List result = new ArrayList<>(); 87 | Stack stack = new Stack<>(); 88 | Node node; 89 | // 先把根节点入栈 90 | stack.push(root); 91 | while (!stack.empty()) { 92 | node = stack.pop(); 93 | if (node != null) { 94 | // 对于栈中每一个结点node,只要node不为null,就读取node的值,并依次将其右子树和左子树入栈 95 | result.add(node.val); 96 | stack.push(node.right); 97 | stack.push(node.left); 98 | } 99 | } 100 | return result; 101 | } 102 | ``` 103 | 104 | #### 中序遍历:左->根->右 105 | ```java 106 | List inorder(Node root) { 107 | List result = new ArrayList<>(); 108 | Stack stack = new Stack<>(); 109 | Node node; 110 | // 先把根节点入栈 111 | stack.push(root); 112 | while (!stack.empty()) { 113 | // 对于栈中每一个结点node,只要node不为null,就把node入栈并将node置为node.left,然后重复 114 | node = stack.pop(); 115 | while (node != null) { 116 | stack.push(node); 117 | node = node.left; 118 | } 119 | if (!stack.empty()) { 120 | // 直到node.left为空时,栈顶元素就是node,此时读取node的值,并将node.right入栈 121 | node = stack.pop(); 122 | result.add(node.val); 123 | stack.push(node.right); 124 | } 125 | } 126 | return result; 127 | } 128 | ``` 129 | 130 | #### 后序遍历:左->右->根 131 | ```java 132 | List postorderTraversalIteratively(Node root) { 133 | List result = new ArrayList<>(); 134 | Stack stack = new Stack<>(); 135 | Node current, previous = null; 136 | // 先把根节点入栈 137 | stack.push(root); 138 | while (!stack.empty()) { 139 | current = stack.pop(); 140 | if (current == null) continue; 141 | if ((current.left == null && current.right == null) || 142 | (current.left != null && current.left == previous) || 143 | (current.right != null && current.right == previous)) { 144 | // current为叶子结点或current的子树被访问过时,可以访问current 145 | result.add(current.val); 146 | } else { 147 | // 否则就依次把current、current.right、current.left入栈 148 | stack.push(current); 149 | stack.push(current.right); 150 | stack.push(current.left); 151 | } 152 | previous = current; 153 | } 154 | return result; 155 | } 156 | ``` 157 | 158 | #### 层次遍历(BFS) 159 | ```java 160 | List> levelOrder(Node root) { 161 | List> result = new ArrayList<>(); 162 | Queue queue1 = new LinkedList<>(), queue2 = new LinkedList<>(); 163 | // 根结点入队 164 | queue1.offer(root); 165 | // queue1和queue2交替作为from和to,使用一个标记位记录 166 | boolean queue1ToQueue2 = true; 167 | while (true) { 168 | List level; 169 | if (queue1ToQueue2) { 170 | level = traversal(queue1, queue2); 171 | } else { 172 | level = traversal(queue2, queue1); 173 | } 174 | // 本层遍历结果为空时表明遍历结束,退出循环 175 | if (level.isEmpty()) break; 176 | result.add(level); 177 | queue1ToQueue2 = !queue1ToQueue2; 178 | } 179 | return result; 180 | } 181 | 182 | List traversal(Queue from, Queue to) { 183 | // 遍历一层结点,本层结点存在from中,把下一层结点存在to,返回本层遍历结果 184 | List result = new ArrayList<>(); 185 | while (!from.isEmpty()) { 186 | Node node = from.poll(); 187 | if (node != null) { 188 | result.add(node.val); 189 | to.offer(node.left); 190 | to.offer(node.right); 191 | } 192 | } 193 | return result; 194 | } 195 | ``` 196 | 197 | ### 已知前序遍历、中序遍历、后序遍历中的两种,求剩下的一种。 198 | * 前+中=>后:由前序遍历的第一个结点确定根结点,再根据中序遍历中根结点的位置,将中序遍历的结果分割为左子树和右子树,递归求解即可。 199 | * 中+后=>前:由后序遍历的最后一个结点确定根结点,再根据中序遍历中根结点的位置,将中序遍历的结果分割为左子树和右子树,递归求解即可。 200 | * 前+后=>中:中序遍历未知,无法求出确定解。 201 | 202 | ### 求二叉树的深度(高度) 203 | ```java 204 | int height(Node root) { 205 | if (root == null) return 0; 206 | int leftHeight = maxDepth(root.left); 207 | int rightHeight = maxDepth(root.right); 208 | return Math.max(leftHeight, rightHeight) + 1; 209 | } 210 | ``` 211 | 212 | ### 求第K层的结点数 213 | ```java 214 | int getKLevel(Node node, int k) { 215 | if (node == null || k < 1) return 0; 216 | if (k == 1) return 1; 217 | return getKLevel(node.left, k - 1) + getKLevel(node.right, k - 1); 218 | } 219 | ``` 220 | 221 | ### 判断两棵二叉树是否结构相同 222 | ```java 223 | boolean structureCmp(Node node1, Node node2) { 224 | if (node1 == null && node2 == null) return true; 225 | else if (node1 == null || node2 == null) return false; 226 | boolean leftResult = structureCmp(node1.left, node2.left); 227 | boolean rightResult = structureCmp(node1.right, node2.right); 228 | return leftResult && rightResult; 229 | } 230 | ``` 231 | 232 | ### 求二叉树的镜像 233 | ```java 234 | void mirror(Node node) { 235 | if (node == null) return; 236 | Node temp = node.left; 237 | node.left = node.right; 238 | node.right = temp; 239 | mirror(node.left); 240 | mirror(node.right); 241 | } 242 | ``` 243 | 244 | ### 找出任意两个结点的最低公共父结点(Lowest Common Ancestor,LCA) 245 | ```java 246 | Node lca(Node node, Node target1, Node target2) { 247 | if (node == null) return null; 248 | if (node == target1 || node == target2) return node; 249 | Node leftResult = findLCA(node.left, target1, target2); 250 | Node rightResult = findLCA(node.right, target1, target2); 251 | if (leftResult != null && rightResult != null) { 252 | // 分别在左右子树 253 | return node; 254 | } else { 255 | // 都在左子树或右子树 256 | return leftResult != null ? leftResult : rightResult; 257 | } 258 | } 259 | ``` 260 | 261 | ### 求任意两个结点的距离 262 | ```java 263 | int getLevel(Node node, Node target) { 264 | // 求node和target的层数差 265 | if (node == null) return -1; 266 | if (node == target) return 0; 267 | int level = getLevel(node->left, target); 268 | // 先在左子树找 269 | if (level == -1) 270 | // 如果左子树没找到,在右子树找 271 | level = getLevel(node->right, target); 272 | if (level != -1) 273 | // 找到了,回溯 274 | return level + 1; 275 | // 如果左右子树都没找到 276 | return -1; 277 | } 278 | 279 | int twoNodesDistance(Node node, Node target1, Node target2) { 280 | // 先找到最低公共祖先结点 281 | Node lca = findLCA(node, target1, target2); 282 | int level1 = getLevel(lca, target1); 283 | int level2 = getLevel(lca, target2); 284 | return level1 + level2; 285 | } 286 | ``` 287 | 288 | ### 找出二叉树中某个结点的所有祖先结点 289 | ```java 290 | boolean findAllAncestors(Node node, Node target) { 291 | if (node == null) return false; 292 | if (node == target) return true; 293 | if (findAllAncestors(node.left, target) || FindAllAncestors(node.right, target)) { 294 | // 找到了,输出并回溯 295 | System.out.println(node.data); 296 | return true; 297 | } 298 | //如果左右子树都没找到 299 | return false; 300 | } 301 | ``` 302 | 303 | ### 二分查找树转化为排序的循环双链表 304 | ```java 305 | Node leftTail; 306 | public Node Convert(Node root) { 307 | if (root == null) return null; 308 | // 将左子树构造成双链表,并返回链表头节点。 309 | Node left = Convert(root.left); 310 | // 如果左子树链表不为空,将当前root追加到左子树链表的最后一个节点,使用leftTail记录。 311 | if (left == null) { 312 | left = root; 313 | root.left = null; 314 | leftTail = left; 315 | } else { 316 | leftTail.right = root; 317 | root.left = leftTail; 318 | leftTail = root; 319 | } 320 | // 将右子树构造成双链表,并返回链表头节点。 321 | Node right = Convert(root.right); 322 | // 如果右子树链表不为空的话,将该链表追加到root节点之后。 323 | if (right != null) { 324 | right.left = root; 325 | root.right = right; 326 | } 327 | // 根据左子树链表是否为空确定返回的节点。 328 | return left; 329 | } 330 | ``` 331 | 332 | ### 有序链表转化为平衡的二分查找树 333 | 采用自顶向下的方法,先使用快慢指针找到链表的中间节点作为二叉树的根节点,然后递归左右两部分。 334 | 335 | -------------------------------------------------------------------------------- /基于多核的并行编程/Ch2-1 Windows多核编程.md: -------------------------------------------------------------------------------- 1 | # Ch2-1 Windows多核编程 2 | 3 | ## 进程 4 | 5 | ### 进程的概念 6 | 进程是一个可以并发执行的具有独立功能的程序关于某个数据集合的一次执行过程,也是操作系统进行资源分配和保护的基本单位。 7 | 8 | ### 进程的组成部分 9 | * 内核对象:存放进程的管理信息。 10 | * 独立的地址空间:对同一个进程而言,其地址空间内包含了所有代码段和数据段。 11 | 12 | ### 进程的特点 13 | * 进程具有独立性,也称为异步性。 14 | * 进程是系统中资源分配和保护的基本单位,也是系统调度的独立单位。 15 | * 凡是未建立进程的程序,都不能作为独立单位参与运行。 16 | * 通常每个进程都可以以各自独立的速度在CPU上推进。 17 | * 进程开销较大。 18 | 19 | ## 线程 20 | 21 | ### 线程的概念 22 | 线程是进程中包含的一个或多个执行单元,是CPU调度的最小单位。 23 | 24 | ### 线程的组成部分 25 | * 内核对象:存放线程的管理信息。 26 | * 线程栈:每个线程都要维护一个线程栈,它不需要独立的地址空间,用来保存函数的调用历史、参数信息和局部变量。 27 | 28 | ### Windows单核CPU机器对多线程的支持 29 | 用不同线程的轮转调度来实现多线程。 30 | 31 | ### 线程API的实现层次 32 | * 操作系统层(如WIN32 API) 33 | * 库或运行时环境(MFC、.Net框架) 34 | * 专门的多线程库(pthread) 35 | ## 创建线程:三对API 36 | 37 | ### CreateThread() & ExitThread() 38 | ```C++ 39 | HANDLE CreateThread ( 40 | LPSECURITY_ATTRIBUTES lpThreadAttributes, // 用于确定子线程的权限和安全值,没有特殊需要时,提供NULL即可。 41 | DWORD dwStackSize, // 线程栈的大小,如果填0则为默认大小。 42 | LPTHREAD_START_ROUTINE lpStartAddress, // 线程主函数的函数指针。 43 | LPVOID lpParameter, // 传递给线程主函数的参数指针。 44 | DWORD dwCreationFlags, // 标志位,0表示定义线程立即执行,CREATE_SUSPENDED表示创建后先挂起。 45 | LPDWORD lpThreadId // 输出参数,表示新线程的ID。 46 | ); 47 | VOID ExitThread (DWORD dwExitCode); 48 | DWORD WINAPI ThreadFunc (LPVOID data) { // 线程函数 return 0; // 退出时会自动地隐式调用ExitThread(0); } 49 | ``` 50 | 51 | * 这对API是最基本、最简单的创建/销毁线程API,其他API都通过这对API来创建/销毁线程,但这对API实际用得较少。 52 | * 句柄(HANDLE)理解为指向一个复杂结构的指针,本身占用的内存不大。 53 | * 返回值是当前线程对象的指针。 54 | * 所有LP类型都是对应类型的指针,如LPVOID就是void*。 55 | * DWORD表示双字长整数。 56 | 57 | ### _beginthreadex() & _endthreadex() 58 | ```C++ 59 | unsigned long _beginthreadex ( void *security, unsigned stack_size, unsigned (*start_address) (void *), void *arglist, 60 | unsigned initflag, 61 | unsigned *threadID 62 | ); 63 | void _endthreadex (unsigned retval); 64 | ``` 65 | 66 | * 使用最广泛的创建/销毁线程API,调用上一对API实现,有进一步的封装。 67 | * 参数类型与功能和上一对API没有任何区别。 68 | * 安全性比上一对API更好,因为上下文增加了对多个线程同时调用库函数时内存泄漏和访问冲突的检查和预防。 69 | * 这对API存在于C/C++运行时库的多线程版本中。 70 | 71 | ### AfxBeginThread() & AfxEndThread() 72 | ```C++ 73 | CWinThread* AfxBeginThread ( 74 | AFX_THREADPROC pfnThreadProc, 75 | LPVOID pParam, 76 | int nPriority = THREAD_PRIORITY_NORMAL, 77 | UINT nStackSize = 0, 78 | DWORD dwCreateFlags = 0, 79 | LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ); 80 | CWinThread* AfxBeginThread ( CRuntimeClass* pThreadClass, 81 | int nPriority = THREAD_PRIORITY_NORMAL, 82 | UINT nStackSize = 0, 83 | DWORD dwCreateFlags = 0, 84 | LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL 85 | ); 86 | ``` 87 | 88 | * 用于MFC的一对线程API。 89 | 90 | ## 获取当前进程/线程信息 91 | ```C++ 92 | HANDLE GetCurrentProcess(); 93 | HANDLE GetCurrentThread(); 94 | 95 | DWORD GetCurrentProcessId(); 96 | DWORD GetCurrentThreadId(); 97 | ``` 98 | ## 管理线程 99 | ```C++ 100 | DWORD SuspendThread (HANDLE hThread); // 挂起线程 101 | DWORD ResumeThread (HANDLE hThread); // 恢复线程 102 | BOOL TerminateThread (HANDLE hThread, DWORD dwExitCode); // 中止线程 103 | ``` 104 | 105 | * 以线程句柄hThread为参数 106 | * 挂起和恢复的实现机制:内核维护一个挂起计数。 107 | * 手动中止线程需要当心内存泄漏。 108 | * 挂起或中止线程的危险性:挂起、中止线程时不会自动释放资源,从而共享资源或锁的占用会导致死锁发生。 109 | ## 线程同步 110 | 111 | ### 用户态机制 112 | 互锁函数只能在单值上运行;临界区只能对单个进程中的线程同步,但开销小,速度快。 113 | 114 | #### 互锁函数 115 | * 以原子操作的方式修改一个值。 116 | * 相比其它同步(互斥)方式,速度极快,是WinAPI中最快的同步方式。 117 | * 实现时,互锁函数对总线发出硬件信号,防止另一个CPU访问同一内存地址。 118 | 119 | ```C++ 120 | InterlockedIncrement (PLONG); // 使一个LONG变量加1。 121 | InterlockedDecrement (PLONG); // 使一个LONG变量减1。 122 | InterlockedExchangeAdd (PLONG, LONG); // 将参数2加到参数1指向的变量中。 123 | InterlockedExchange (PLONG, PLONG); // 将参数2的值赋给参数1指向的值,返回原始值。 124 | InterlockedExchangePointer (PVOID*, PVOID); // 功能同上。 125 | InterlockedCompareExchange (PLONG, LONG, LONG); // 将参数1指向的值与参数3比较,相同则把参数2的值赋给参数1指向的值,不同则不变。 126 | InterlockedCompareExchangePointer (PVOID*, PVOID, PVOID); // 功能同上。 127 | ``` 128 | 129 | #### 自旋锁/循环锁(Spin Lock) 130 | * 非阻塞锁:由某个线程独占,采用循环锁时,等待线程并不是静态地阻塞在同步点,而是必须不断循环尝试直到获得该锁。 131 | * 用于锁持有时间(受保护资源使用时间)较短的情况。 132 | * 避免在单CPU或单核计算机中使用循环锁,因为此锁占用CPU很大。 133 | * 使用场景: 134 | * 预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少,则使用自旋锁。 135 | * 预计线程等待锁的时间较⻓,至少比两次线程上下文切换的时间要⻓,则使用互斥量而不是自旋锁。 136 | * 如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,因为自旋锁的开销比较小,互斥量的开销较大。 137 | * 可以用InterlockedExchange来实现自旋锁: 138 | 139 | ```C++ 140 | BOOL resourceInUse = FALSE; void func() { 141 | // 不断循环尝试获取锁,直到获取了锁才能进入下一步操作。 While (InterlockedExchange(&resourceInUse, TRUE) == TRUE) sleep(0); // 已获取到锁,接下来完成需要执行的操作。 ...... 142 | // 完成操作后释放锁。 143 | InterlockedLockedExchange(&resourceInUse, FALSE); } 144 | ``` 145 | 146 | #### 临界区对象(CRITICAL_SECTION) 147 | 保证临界区内所有被访问的资源不被其它线程访问,直到当前线程执行完临界区代码。 148 | 149 | ```C++ 150 | // 临界区相关API 151 | void InitializeCriticalSection (LPCRITICAL_SECTION); 152 | void EnterCriticalSection (LPCRITICAL_SECTION); 153 | void LeaveCriticalSection (LPCRITICAL_SECTION); 154 | void DeleteCriticalSection (LPCRITICAL_SECTION); 155 | 156 | // 使用临界区的例子 157 | void example() { 158 | CRITICAL_SECTION cs; 159 | InitializeCriticalSection (&cs); 160 | try { 161 | EnterCriticalSection (&cs); 162 | // Do something 163 | } finally { 164 | LeaveCriticalSection (&cs); 165 | } 166 | // try-catch机制用于防止代码的异常导致其它线程继续被阻塞。 167 | } 168 | ``` 169 | 170 | ### 内核态机制 171 | * 适应性广泛,但开销大,速度慢。 172 | * 可用于同步的内核对象: 173 | * Processes, Threads, Jobs, Files 174 | * Events 175 | * Semaphore, Mutexes 176 | * File change notifications 177 | * Waitable timers 178 | * Console input 179 | * 每个内核对象,要么处于触发状态,要么处于未触发状态。 180 | 181 | #### 等待函数(Wait Function) 182 | 线程可以等待直到内核对象触发了才继续执行。 183 | 184 | ```C++ 185 | DWORD WaitForSingleObject ( 186 | HANDLE hObject, // 内核对象的HANDLE 187 | DWORD dwMilliseconds // 最大等待时长 188 | ); 189 | 190 | // 使用WaitForSingleObject的例子 191 | DWORD dw = WaitForSingleObject (hProcess, 5000); 192 | switch (dw) { case WAIT_OBJECT_0: 193 | // The process terminated. 194 | break; case WAIT_TIMEOUT: 195 | // The process did not terminated within 5000 milliseconds. break; case WAIT_FAILED: 196 | // bad call to function (invalid handle?) break; 197 | } 198 | 199 | DWORD WaitForMultipleObjects ( 200 | DWORD dwCount, // 等待的事件个数 201 | CONST HANDLE *phObjects, // 第一个内核对象的HANDLE指针 202 | BOOL fWaitAll, // TRUE表示等待所有对象变为已通知状态,FALSE表示等待任一个对象变为已通知状态 203 | DWORD dwMilliseconds // 最大等待时长 204 | ); 205 | 206 | // 使用WaitForMultipleObjects的例子 207 | HANDLE h[3]; h[0] = hProcess1; 208 | h[1] = hProcess2; 209 | h[3] = hProcess3; 210 | DWORD dw = WaitForMultipleObject(3, h, FALSE, 5000); 211 | switch (dw) { case WAIT_TIMEOUT: // The process did not terminated within 5000 milliseconds. break; case WAIT_FAILED: // bad call to function (invalid handle?) break; 212 | case WAIT_OBJECT_0 + 0: 213 | // The process identified by h[0] (hProcess1) terminated. break; case WAIT_OBJECT_0 + 1: // The process identified by h[1] (hProcess2) terminated. break; case WAIT_OBJECT_0 + 2: // The process identified by h[2] (hProcess3) terminated. break; 214 | } 215 | ``` 216 | 217 | * 等待函数的副作用: 218 | * 在等待内核对象的过程中,等待函数会将已经触发的内核对象重置为未触发状态。 219 | * 可能会造成死锁。 220 | * 两个API的返回值都表示等待结束的原因: 221 | * WaitForSingleObject的返回值有三种:`WAIT_OBJECT_0`(等待的事件发生了)、`WAIT_TIMEOUT`(时间到了)、`WAIT_FAILED`(出错了,比如内核对象在等待过程中被释放等情况)。 222 | * WaitForMultipleObjects的返回值有:`WAIT_OBJECT_0 + i`(i表示第i个事件发生了)、`WAIT_TIMEOUT`(时间到了)、`WAIT_FAILED`(出错了,比如内核对象在等待过程中被释放等情况)。 223 | 224 | #### 事件对象(Event) 225 | * 是Win32提供的最灵活的线程间同步方式。 226 | * 事件的状态包括两种:激发与未激发。 227 | * 事件的分类: 228 | * 手动设置:只能用程序来手动设置,在需要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。 229 | * 自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。 230 | 231 | ```C++ 232 | // 创建事件 233 | HANDLE CreateEvent ( 234 | PSECURITY_ATTRIBUTES psa, // 安全属性,同创建线程,通常置NULL 235 | BOOL fManualReset, // 是否需要手动设置 236 | BOOL fInitialState, // 事件初始状态 237 | PCTSTR pszName // 好像是事件名称吧 238 | ); 239 | // 设置事件为激发状态 240 | BOOL SetEvent (HANDLE hEvent); 241 | // 设置事件为未激发状态 242 | BOOL ResetEvent (HANDLE hEvent); 243 | ``` 244 | 245 | #### 信号量对象(Semaphore) 246 | ```C++ 247 | // 创建信号量 248 | HANDLE CreateSemaphore ( 249 | LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性,同创建线程,通常置NULL 250 | LONG lInitialCount, // 信号量初始计数 251 | LONG lMaximumCount, // 最大计数 252 | LPCTSTR lpName // 好像是信号量名称吧 253 | ); 254 | // 测试信号量:使用WaitForSingleObject(); 255 | // 释放信号量 256 | BOOL ReleaseSemaphore ( 257 | HANDLE hSemaphore, // 要释放的信号量HANDLE 258 | LONG lReleaseCount, // 要释放的计数数量 259 | LPLONG lpPreviousCount // 输出参数,返回释放前信号量的计数 260 | ); 261 | ``` 262 | 263 | #### 互斥量对象(Mutex) 264 | ```C++ 265 | // 创建互斥量 266 | HANDLE CreateMutex ( 267 | LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性,同创建线程,通常置NULL 268 | BOOL bInitialOwner, // 表示当前线程是否是该互斥量持有者 269 | LPCTSTR lpName // 好像是互斥量名称吧 270 | ); 271 | // 测试互斥量:使用WaitForSingleObject(); 272 | // 释放互斥量 273 | BOOL ReleaseMutex (HANDLE hMutex); 274 | ``` 275 | 276 | ## 其他线程相关函数 277 | ### 线程池 278 | * 一些应用需要动态创建线程,如Web服务器。 279 | * 由于创建/销毁线程的开销很大,所以预先将这批线程创建好,需要使用时就分配相应任务,使用完后放回池中等待下次使用。 280 | * 由操作系统提供支持:QueueUserWorkItem()。 281 | 282 | ```C++ 283 | BOOL QueueUserWorkItem( 284 | LPTHREAD_START_ROUTINE Function, // 所有线程的入口函数 285 | PVOID Context, // 线程主函数的参数 286 | ULONG Flags // 控制符,可以用来释放池中的线程或创建新线程,默认置0 287 | ); 288 | ``` 289 | 290 | * 第一次调用此API时,Windows创建一个线程池,其中一个线程执行Function函数,此线程的任务结束后后,将返回线程池,等待新任务。 291 | * Windows内部的调度算法决定处理当前线程工作负载的最佳方式。 292 | ### 线程优先级 293 | 线程优先级 = 进程优先级 + 线程相对优先级 294 | 295 | ```C++ 296 | Bool SetThreadPriority ( 297 | HANDLE hThread, // 要设置优先级的线程HANDLE 298 | int nPriority // 要设置的优先级 299 | ); 300 | // 返回值为是否设置成功 301 | ``` 302 | 303 | ### 处理器亲和 304 | * 当线程被调度执行时,操作系统确定线程运行在哪个处理器上。 305 | * 亲和:线程对其所运行的处理器的有限选择。 306 | * 例如I/O密集型线程与计算密集型线程的搭配。 307 | * 指定处理器亲和的效果必须谨慎。 308 | 309 | ```C++ 310 | DWORD_PTR SetThreadAffinityMask ( 311 | HANDLE threadHandle, 312 | DWORD_PTR threadAffinityMask 313 | ); 314 | ``` 315 | 316 | * 线程的AffinityMask必须是所属进程AffinityMask的子集。 317 | * DWORD_PTR是一个二进制串,直接写十六进制常数即可,其中每个bit表示一个核是否使用,希望谁使用,就置相应位为1。 318 | * 设置多个核时,就由系统在这些核中进行调度和切换。 319 | 320 | ## 线程局部存储(Thread Local Storage,TLS) 321 | 322 | ### TLS介绍 323 | * 方便的存储线程局部数据的系统。 324 | * 可使用TLS将数据与特定的线程相关联。 325 | * 利用TLS机制可以为进程中的所有线程关联若干个数据,各个线程通过TLS分配的全局索引来访问自己关联的数据。 326 | * 本质上仍然是全局变量。 327 | 328 | ### Windows的TLS实现 329 | * 定义了一个数组,需要使用时向其申请一个项。 330 | * 早期时,项的数量为64。 331 | * 一旦TLS被启用,此变量将使得每个线程都拥有一份。 332 | * 每个线程有其私有空间,其中的数组与公有空间的数组长度对应。 333 | * 公有空间中将相应元素置为FREE或INUSE: 334 | * INUSE表示私有空间中的数组上有值。 335 | * FREE表示私有空间上的位置没有被使用。 336 | * 默认情况下,所有私有空间都没有被使用。 337 | 338 | ### 涉及到的API 339 | 340 | #### TlsAlloc():分配一个空闲项,返回对应下标。 341 | ```C++ 342 | DWORD WINAPI TlsAlloc (void); 343 | ``` 344 | 345 | * 每个进程都维护一个长度为`TLS_MINUMUM_AVAILABLE`的位数组,此位数组用于记录哪个下标对应的项被使用,哪个下标对应的项空闲,TlsAlloc的返回值就是数组的一个下标。 346 | * 初始状态下,此位数组成员的值都是FREE,当调用TlsAlloc时直接遍历数组寻找一个空闲项,直到找到一个值为FREE的成员,然后把找到的成员的值由FREE改成INUSE后,TlsAlloc返回该成员的索引。 347 | * 如果找不到一个为FREE的项,则返回`TLS_OUT_OF_INDEXES`,表示分配失败。 348 | * 当一个线程被创建时,Windows就会在进程地址空间中为该线程分配一个长度为`TLS_MINIMUM_AVAILABLE`的数组,数组成员的值都初始化为0,在内部系统将此数组与该线程关联起来,保证只能在该线程内访问此数组中的数据。 349 | 350 | #### TlsSetValue()与TlsGetValue():对应项的存取 351 | ```C++ 352 | BOOL WINAPI TlsSetValue ( 353 | __in DWORD dwTlsIndex, // 索引值,表示在数组中的具体位置 354 | __in_opt LPVOID lpTlsValue // 要设置的值 355 | ); 356 | 357 | LPVOID WINAPI TlsGetValue ( 358 | __in DWORD dwTlsIndex // 索引值 359 | ); 360 | ``` 361 | 362 | * 如果调用成功则返回TRUE,否则返回FALSE。 363 | * 只能改变线程自身的数组,不能设置其它线程的值。 364 | * 存储的值一般为指针。 365 | 366 | #### TlsFree():释放一个项 367 | ```C++ 368 | BOOL WINAPI TlsFree ( 369 | __in DWORD dwTlsIndex // 索引值 370 | ); 371 | ``` 372 | 373 | * 实现时直接将全局的位数组对应位置的INUSE改为FREE。 374 | * 如果试图释放一个未被分配的槽将产生一个错误。 375 | 376 | -------------------------------------------------------------------------------- /基于多核的并行编程/Ch2-2 Linux多核编程.md: -------------------------------------------------------------------------------- 1 | # Ch2-2 Linux多核编程 2 | 3 | ## Linux进程 4 | 5 | ### C程序的入口 6 | * crt0.o:此文件中的内容是固定的,在这里调用main函数,因此main函数返回之后还能进行一些处理工作。 7 | * cc/ld时自动链接。 8 | * main函数原型: 9 | 10 | ```C++ 11 | int main (int argc char *argv[]); 12 | ``` 13 | 14 | ### exec系列函数 15 | 16 | #### 介绍 17 | * exec系列函数都用于启动一个新程序,切换到新的上下文。 18 | * 废弃原程序的数据段堆栈段,为新程序分配新的数据段和堆栈段,仅有进程号不变,返回后继续执行原来的任务。 19 | * 返回值为当前进程号(不变)。 20 | 21 | #### 函数原型 22 | #include 23 | 24 | int execl (const char *path, const char *arg0, ..., (char *)0); 25 | int execlp (const char *file, const char *arg0, ..., (char *)0); 26 | int execle (const char *path, const char *arg0, ..., (char *)0, char *const envp[]); 27 | int execv (const char *path, char *const argv[]); 28 | int execvp (const char *file, char *const argv[]); 29 | int execve (const char *path, char *const argv[], char *const envp[]); 30 | 31 | * 带l的exec:参数可变。 32 | * 带p的exec:第一个参数是文件名。 33 | * 带e的exec:可以修改当前的环境变量。 34 | 35 | ### fork函数 36 | 37 | #### 介绍 38 | * 执行fork后有两个线程执行同一段代码。 39 | * 原进程的返回值为新进程的PID;新进程的返回值为0。 40 | 41 | #### 函数原型 42 | #include 43 | #include 44 | 45 | pid_t fork(); 46 | 47 | #### 使用范例 48 | ```C++ 49 | if (fork()==0) { 50 | // 子进程代码段 51 | } else { 52 | // 父进程代码段 53 | } 54 | ``` 55 | 56 | ### 结束进程 57 | 58 | #### 正常退出 59 | * 从main函数返回。 60 | * 调用`exit`函数或`_exit`函数。 61 | * `exit`是C库函数,`_exit`是系统调用,其函数原型如下: 62 | 63 | #include 64 | void exit (int status); 65 | #include 66 | void _exit (int status); 67 | 68 | * `_exit`更贴近底层,可以立即结束一个进程,而`exit`还会做一些清理工作(调用各种终止处理程序,清理标准I/O,最终才调用`_exit`)。 69 | * 终止处理程序:可以自行编写,使用`atexit`函数,意思是注册一个在程序正常退出时执行的程序,本质上是注册处理程序的入口地址。 70 | 71 | #include 72 | int atexit (void (*function) (void)); 73 | 74 | * 多线程程序的最后一个线程退出。 75 | 76 | #### 非正常结束 77 | * 调用`abort`函数。 78 | * 收到`kill`信号。 79 | * 最后一个线程被取消。 80 | 81 | ### wait()与waitpid() 82 | 83 | #### 使用背景 84 | * 主要用于父子进程之间,等待进程结束。 85 | * Linux下每个进程在执行完毕后并不立即回收,只有在其父进程调用wait()/waitpid()时才会将其资源释放出来。 86 | 87 | #### 函数原型 88 | #include 89 | #include 90 | pit_t wait (int *status); // 等待任意一个子进程结束。 91 | pid_t waitpid (pid_t pid, int *status, int options); // 等待指定pid的子进程结束。 92 | 93 | #### 参数含义 94 | * status是输出参数,用于收集被等待进程的返回值等信息。 95 | * options可以使waitpid非阻塞,置为`WNOHANG`即可,即调用的时候地看一下当前是否存在符合条件的僵尸进程,看完立即返回。 96 | * 返回值是寻找到的僵尸进程的pid,如果pid不存在则返回-1。 97 | * waitpid()的pid参数: 98 | * -1:waitpid()此时退化为wait(),等待任一子进程结束。 99 | * >0:等待对应PID的子进程。 100 | * =0:等待与当前进程同组的子进程。 101 | * <0:此参数的绝对值代表进程组组号,等待对应进程组的子进程。 102 | 103 | ## Linux信号 104 | 105 | ### 信号的概念 106 | * 信号本质上是一个软中断,提供了处理异步事件的机制。 107 | * 每个信号都有以SIG开头的名字,信号被定义为正整数(在中)。 108 | 109 | ### 产生信号的方法 110 | * 按终端键,如Ctrl+C -> SIGINT。 111 | * 硬件异常,如内存错误 -> SIGSEGV。 112 | * kill(2)函数。 113 | * kill(1)命令。 114 | * 软件条件:某种软件条件已经发生,并应将其通知有关进程->SIGALARM。 115 | 116 | ### 系统预定义的信号 117 | > Linux支持的所有信号可以通过`man 7 signal`进行查询。 118 | 119 | | 名称 | 说明 | 120 | | --- | --- | 121 | | SIGABRT | 进程异常终止(调用abort函数产生此信号) | 122 | | SIGALRM | 超时(alarm) | 123 | | SIGFPE | 算术运算异常(除0、浮点溢出等) | 124 | | SIGHUP | 连接断开 | 125 | | SIGILL | 非法硬件指令 | 126 | | SIGINT | 终端中断符(Ctrl+C) | 127 | | SIGKILL | 终止(不能被捕捉或忽略) | 128 | | SIGPIPE | 向没有读进程的管道写数据 | 129 | | SIGQUIT | 终端退出符(Ctrl+\) | 130 | | SIGTERM | 终止(由kill命令发出的系统默认终止信号) | 131 | | SIGUSR1 | 用户定义信号 | 132 | | SIGUSR2 | 用户定义信号 | 133 | | SIGSEGV | 无效存储访问(段异常) | 134 | | SIGCHLD | 子进程停止或退出 | 135 | | SIGCONT | 使暂停进程继续 | 136 | | SIGSTOP | 停止(不能被捕捉或忽略) | 137 | | SIGTSTP | 终端挂起符(Ctrl+Z) | 138 | | SIGTTIN | 后台进程请求从控制终端读 | 139 | | SIGTTOUT | 后台进程请求从控制终端写 | 140 | 141 | ### 信号的处理方式 142 | * 忽略信号(SIGKILL、SIGSTOP等严重错误不能被忽略) 143 | * 执行系统默认动作 144 | * 捕捉信号 145 | 146 | ### signal函数:捕捉信号的处理函数 147 | #include 148 | typedef void (*sighandler_t) (int); 149 | sighandler_t signal (int signum, sighandler_t handler); 150 | 151 | * 如果函数调用成功,则返回之前的信号处理函数。 152 | * 如果出错,则返回SIG_ERR。 153 | * handler参数: 154 | * 用户定义的函数 155 | * SIG_DEF(默认信号处理) 156 | * SIG_IGN(忽略) 157 | * 使用示例: 158 | 159 | ```C++ 160 | static void sig_usr (int); 161 | 162 | int main() { 163 | if (signal(SIGUSR1, sig_usr)==SIG_ERR) 164 | err_sys("can't catch SIGUSR1"); 165 | if (signal(SIGUSR2, sig_usr)==SIG_ERR) 166 | err_sys("can't catch SIGUSR2"); 167 | // continue... 168 | } 169 | ``` 170 | 171 | ### 信号的可靠性 172 | * 信号有可能在到达激活的响应函数前被丢弃。 173 | * 现代Linux将信号分为可靠信号和不可靠信号,SIGRTMIN(通常为32)被认为是不可靠信号,其它的是可靠信号。 174 | * 不可靠信号可能会丢失,如多个信号同时到达时,只收到第一个到达的信号,其他的信号全部丢失。 175 | * 线程十分忙时收到信号,信号也可能丢失。 176 | * 信号的复位机制:捕捉信号并处理后,响应函数立即被重置,改回原有的默认函数,如果还需要继续处理,就要在处理函数中重新注册一次。 177 | 178 | ### 中断系统调用 179 | * 低速系统调用在接受到信号时会被中断。 180 | * 此时返回出错,errno=EINTR。 181 | * 解决方法示例: 182 | 183 | ```C++ 184 | again: 185 | if ((n==read(fd, buf, BUFFSIZE))<0) { 186 | if (errno==EINTR) // just an interrupted system call 187 | goto again; 188 | // Handle other errors 189 | } 190 | ``` 191 | 192 | ### 可重入函数和不可重入函数 193 | * 二者区别:可重入函数就是可以被中断的函数,不可重入函数就是不能被中断的函数。 194 | * 不可重入函数: 195 | * 系统资源。 196 | * 全局变量诸如++、--之类的非原子操作。 197 | * 使用静态数据结构。 198 | * 调用malloc或free。 199 | * 标准I/O函数。 200 | * 使用说明:尽量不要在信号处理函数中使用不可重入函数,可能有一定概率出错。这是因为主函数无法控制信号产生的时机,因而调用这些不可重入函数的中间状态不可预料,从而对内核产生不可预料的影响。 201 | 202 | ### 发送信号 203 | 发送信号有权限要求,低权限用户不能向高权限用户进程发送信号,root可以给所有进程发信号。 204 | 205 | #### kill(2):向一个指定进程发送信号 206 | #include 207 | #include 208 | 209 | int kill (pid_t pid, int sig); 210 | 211 | * 可以使用`kill`发送任何信号。 212 | * pid一样可以多样取值,和waitpid相同。 213 | * 成功返回0,失败返回-1。 214 | 215 | #### raise(3):向当前的进程(自己)发送信号 216 | #include 217 | 218 | int raise (int sig); 219 | 220 | * 成功返回0,失败返回-1。 221 | 222 | #### alarm:过一段时间向自己发送SIGALARM信号(类似闹钟) 223 | #include 224 | 225 | unsigned int alarm (unsigned int seconds); 226 | 227 | * alarm主要用于对可能阻塞的操作做时间限制。 228 | * 没有设定处理函数时,默认行为为退出。 229 | * 连续发送SIGALARM信号时,最新的alarm将替换掉原有的,返回值为上次调用的剩余倒计时;第一次调用时返回0;调用失败返回-1。 230 | 231 | #### pause:停下来等待一个信号 232 | #include 233 | 234 | int pause (void); 235 | 236 | * 返回-1,并且errno被设置为EINTR。 237 | 238 | #### 使用alarm与pause来实现sleep 239 | ```C++ 240 | void sig_alarm (int signo) { 241 | printf("alarm received.\n"); 242 | } 243 | 244 | unsigned int sleep1 (unsigned int nsecs) { 245 | if (signal(SIGALARM, sig_alarm)==SIG_ERR) 246 | return nsecs; 247 | alarm(nsecs); 248 | pause(); 249 | return alarm(0); // 将闹钟清零从而取消掉 250 | } 251 | ``` 252 | 253 | ## 可靠信号机制 254 | 255 | ### 信号集(信号的集合) 256 | #include 257 | 258 | int sigemptyset (sigset_t *set); 259 | int sigfillset (sigset_t *set); 260 | int sigaddset (sigset_t *set, int signum); 261 | int sigdelset (sigset_t *set, int signum); 262 | // 以上四个函数成功返回0,出错返回-1。 263 | int sigismember (xonst sigset_t *set, int signum); 264 | // 该函数成功返回返回1,出错返回0。 265 | 266 | ### 信号掩码 267 | #include 268 | 269 | int sigprocmask (int how, const sigset_t *set, sigset_t *oldset); 270 | // 成功返回返回0,出错返回-1。 271 | 272 | * 用于检测或更改进程的信号掩码。 273 | * 参数oldset将返回原有的信号集。 274 | * 参数how有三种: 275 | * SIG_BLOCK:将信号并入掩码。 276 | * SIG_UNBLOCK:在掩码中除去相应的信号。 277 | * SIG_SETMASK:直接替换。 278 | * 在sigprocmask调用后,任何未阻塞并且pending的信号,在函数返回前,至少有一个信号会送达进程。 279 | * 所有信号集内的信号都会被屏蔽,但将会被置为pending,更多相同类型的信号将被丢弃。 280 | * 被屏蔽的信号重新打开时,信号将被处理,并且一定在sigprocmask返回之前被处理 281 | * 注意:这两个信号SIGKILL、SIGSTOP无法被阻塞。 282 | 283 | ### sigpending() 284 | 返回当前未决的信号集,成功返回返回0,出错返回-1。 285 | 286 | #include 287 | int sigpending (sigset_t *set); 288 | // 输出参数set用来输出信号集。 289 | 290 | ### sigaction() 291 | 用于检查或修改(或两者)与指定信号关联的处理动作,类似系统调用signal,成功返回返回0,出错返回-1。 292 | 293 | #include 294 | 295 | int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact); 296 | 297 | * sigaction中的屏蔽字仅在执行信号处理函数期间生效,当前正在响应的信号被默认直接屏蔽,不需设置。 298 | * sa_flags可以有多种设定参数。 299 | 300 | ### sigsuspend() 301 | 用sigmask临时替换信号掩码,在捕捉一个信号或发生终止进程的信号前,进程挂起,成功返回返回0,出错返回-1。 302 | 303 | #include 304 | int sigsuspend(const sigset *sigmask); 305 | 306 | * 等待信号时,信号可以是之前pending的,也可以是在等待过程中出现的。 307 | * 等待结束后,信号掩码仍然恢复到原先的设定。 308 | 309 | ## 共享内存 310 | * 共享内存是内核为进程创建的特殊内存段,它可以连接到自己的地址空间,也可以连接到其它进程的地址空间。 311 | * 是最快的进程间通信方式,但不提供任何同步功能。 312 | * 线程共享内存的存储位置一定在内核的内核态。 313 | 314 | ## POSIX线程(pthread) 315 | 316 | ### 配置编程环境 317 | * pthread库: 318 | * /usr/lib/libpthread.so 319 | * /usr/lib/libpthread.a 320 | * pthread.h头文件:/usr/include/pthread.h 321 | * 编译选项:gcc thread.c -o thread -lpthread 322 | 323 | ### 线程的创建和结束 324 | 325 | #### 创建线程 326 | #include 327 | 328 | int pthread_create ( 329 | pthread_t *thread, 330 | pthread_attr_t *attr, 331 | void *(start_routine) (void *), 332 | void *arg 333 | ); 334 | 335 | #### 结束当前线程 336 | #include 337 | 338 | void pthread_exit (void *retval); 339 | 340 | ### Joinable与Detached线程 341 | ![Joinable与Detached线程](media/Linux多核编程/Joinable与Detached线程.jpg) 342 | 343 | int pthread_join (pthread_t th, void **thread_return); 344 | int pthread_detach (pthread_t th); 345 | 346 | * 主线程默认等待所有子线程结束后才结束。 347 | * 如果要关闭这一特性,需要将子线程设定为Detached。 348 | * Linux下线程没有严格的主次之分。 349 | 350 | ### 线程同步 351 | 352 | #### 信号量(Semaphore) 353 | #include 354 | 355 | int sem_init (sem_t *sem, int pshared, unsigned int value); 356 | int sem_wait (sem_t *sem); 357 | int sem_post (sem_t *sem); 358 | int sem_destroy (sem_t *sem); 359 | int sem_trywait (sem_t *sem); 360 | int sem_getvalue (sem_t *sem, int *sval); 361 | 362 | * sem_trywait在信号量为0时,不会阻塞等待而立即失败返回,返回值为`EAGAIN`。 363 | 364 | #### 互斥量(Mutex) 365 | #include 366 | 367 | int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); 368 | int pthread_mutex_lock (pthread_mutex_t *mutex); 369 | int pthread_mutex_unlock (pthread_mutex_t *mutex); 370 | int pthread_mutex_destroy (pthread_mutex_t *mutex); 371 | int pthread_mutex_trylock (pthread_mutex_t *mutex); 372 | 373 | * 静态初始化:`PTHREAD_MUTEX_INITIALIZER` 374 | * 动态初始化:`pthread_mutex_init()` 375 | 376 | #### 条件变量(Conditional Variable,CV) 377 | * 全局可见,可被多个线程访问。 378 | * 检查条件是否被满足,基于轮询或类似于Windows的事件机制。 379 | * 条件变量常与互斥量一同使用。 380 | * 条件变量必须声明为pthread_cond_t类型,并且必须在使用之前进行初始化。 381 | * 条件变量提供了unlock与wait的复合原子操作,多线程的互斥全部可以应用此模式。 382 | 383 | ##### 初始化条件变量 384 | ```C++ 385 | // 静态初始化 386 | pthread_cond_t convar = PTHREAD_COND_INITIALIZER; 387 | // 注:一个宏并不只对应一个条件变量,可以被多次调用。 388 | 389 | // 动态初始化 390 | int pthread_cond_init ( 391 | pthread_cond_t *cond, 392 | pthread_condattr_t *cond_attr 393 | ); 394 | // 第二个参数在很多库里没有被实现,直接提供NULL即可。 395 | ``` 396 | 397 | > 静态初始化的条件变量不需要释放,但动态初始化的要销毁。 398 | 399 | ##### 销毁条件变量 400 | ```C++ 401 | int pthread_cond_destroy (pthread_cond_t *cond); 402 | ``` 403 | 404 | ##### 等待、通知、广播 405 | ```C++ 406 | int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex); 407 | int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t &mutex, time_t *timeout); 408 | int pthread_cond_signal (pthread_cond_t cond); 409 | int pthread_cond_broadcast (pthread_cond_t cond); 410 | ``` 411 | 412 | * wait操作必须在互斥量锁上时调用,调用之后在等待过程中会自动释放互斥量,在被唤醒时互斥量又会被自动锁上,因此最终一定要手动再释放一次互斥量。 413 | * signal操作必须在互斥量锁上时调用,用来通知处于等待状态的一个随机条件变量,但无法控制具体是哪个,调用完之后需要为wait操作锁定互斥量。 414 | * broadcast操作将唤醒所有处于等待状态的条件变量。 415 | 416 | ##### 使用模式 417 | * 主线程: 418 | 1. 声明并初始化需要同步的全局数据和变量 419 | 2. 声明并初始化条件变量对象 420 | 3. 声明并初始化相关的互斥量 421 | 4. 创建工作线程 422 | 5. 等待并继续 423 | * 工作线程1: 424 | 1. 工作,直到满足一个特定的条件 425 | 2. 锁上对应的互斥量并检查全局数据的值 426 | 3. 等待条件变量(此时互斥量自动解锁) 427 | 4. 手动地锁上互斥量 428 | 5. 继续工作 429 | * 工作线程2: 430 | 1. 工作 431 | 2. 锁住相关的互斥量 432 | 3. 改变全局数据的值 433 | 4. 检查被等待的全局数据的值,如果满足条件,唤醒一个工作线程1 434 | 5. 解锁信号量 435 | 6. 继续工作 436 | 437 | ### 线程属性对象(pthread_attr_t) 438 | 439 | #### 初始化 440 | #include 441 | 442 | int pthread_attr_init (pthread_attr_t *attr); 443 | 444 | #### get/set族函数 445 | ```C++ 446 | int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate); 447 | int pthread_attr_getdetachstategetdetachstate (const pthread_attr_t *attr, int *detachstate); 448 | 449 | int pthread_attr_setschedpolicy (pthread_attr_t *attr, int policy); 450 | int pthread_attr_getschedpolicy (const pthread_attr_t *attr, int *policy); 451 | 452 | int pthread_attr_setschedparam (pthread_attr_t *attr, int param); 453 | int pthread_attr_getschedparam (const pthread_attr_t *attr, int *param); 454 | ``` 455 | 456 | * detachstate:线程分离,可传入`PTHREAD_CREATE_JOIN`或`PTHREAD_CREATE_DETACHED`。 457 | * schedpolicy:调度策略,可传入`SCHED_OTHER`、`SCHED_RR`或`SCHED_FIFO`,后两个都是实时调度策略。 458 | * schedparam:调度参数,主要是优先级。 459 | 460 | ### 终止线程 461 | ```C++ 462 | int pthread_cancel (pthread_t thread); 463 | int pthread_setcancelstate (int state, int *oldstate); 464 | int pthread_setcanceltype (int type, int *oldtype); 465 | ``` 466 | 467 | * setcancelstate操作用于设置是否理会其它线程的cancel,可传入的state参数为`PTHREAD_CANCEL_ENABLE`或`PTHREAD_CANCEL_DISABLE`。 468 | * setcanceltype操作用于设置中止的时间点,可传入的type参数为`PTHREAD_CANCEL_ASYNCRONOUSD`(立即退出)或`PTHREAD_CANCEL_DEFFERD`(延迟退出)。 469 | 470 | ### 多线程程序容易出现的错误 471 | * 共享变量取法保护(未互斥使用)。 472 | * 创建线程时传递指针,指针指向的变量可能是共享的。 473 | 474 | ### 线程局部存储(Thread Local Storage,TLS) 475 | ```C++ 476 | int pthread_key_create (pthread_key_t *key, void (*destructor) (void*)); 477 | int pthread_key_delete (pthread_key_t key); 478 | void* pthread_getspecific (pthread_key_t key); 479 | int pthread_setspecific (pthread_key_t key, const void *value); 480 | ``` 481 | 482 | * 与Windows的TLS不同之处在于,存入和取出的内容明确为void*类型。 483 | * 可以明确地指定传入数据的析构函数,用于对存入的指针指向的内容进行释放,这里的析构函数在对应线程执行结束后自动执行。 484 | * 释放key时并不会释放key指向的内容,因此这里一定要手动进行释放。 485 | 486 | -------------------------------------------------------------------------------- /Java/Java并发.md: -------------------------------------------------------------------------------- 1 | # Java并发 2 | 3 | ## 基本概念 4 | 5 | ### 进程和线程 6 | 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是资源分配的基本单位,而线程是进程内的基本执行单元。 7 | 8 | ### 同步和异步 9 | 这里说的同步和异步是指方法调用方面。同步调用会等待方法的返回,而异步调用会瞬间返回,但是异步调用瞬间返回并不代表你的任务就完成了,他会在后台起个线程继续进行任务。 10 | 11 | ### 并发和并行 12 | 并行是两个任务同时进行,而并发是一会做一个任务一会又切换做另一个任务。 13 | 14 | ### 临界区 15 | 表示一种公共资源或者说是共享数据/代码,可以被多个线程使用,但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。 16 | 17 | ### 死锁和活锁 18 | 死锁是指两个或以上的任务由于竞争资源而造成的一种阻塞现象,而活锁是指两个或以上的任务都可以使用资源,但他们互相谦让,都想让对方先使用资源,由此造成的一种阻塞现象。 19 | 20 | ## Java线程数量的限制 21 | * 第一个限制在操作系统,操作系统定义了总的最大线程数。 22 | * 第二个限制在JVM,理论上我们能分配给线程的内存除以单个线程占用的内存就是最大线程数。对Java进程来讲,可以大致认为:线程数 = (系统空闲内存 - 堆内存 - 静态方法区内存)/线程栈大小 23 | 24 | ## Java线程的五种状态 25 | 26 | ### 新建(New) 27 | 实现Runnable接口和继承Thread可以得到一个线程类,new一个线程出来就进入了New状态。 28 | 29 | ### 可运行(Runnable) 30 | 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法,就把这个对象对应的线程放入可运行线程池中,等待被线程调度程序选中而获取CPU的使用权。 31 | 32 | ### 运行(Running) 33 | 线程调度程序从可运行线程池中选择一个Runnable线程,使其获取CPU时间片,执行程序代码。这是线程进入Running状态的唯一一种方式。 34 | 35 | ### 阻塞(Blocked) 36 | * 阻塞状态是指线程因为某种原因让出CPU使用权,暂时停止运行。直到线程进入Runable状态,才有机会再次获得CPU时间片。 37 | * 阻塞有三种情况: 38 | * 等待阻塞:线程执行o.wait()方法,JVM会把该线程放入等待队列中。 39 | * 同步阻塞:线程在获取对象的同步锁时,若该同步锁被别的线程占用,JVM会把该线程放入锁池中。 40 | * 其他阻塞:线程执行Thread.sleep(long ms)或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入Runable状态。 41 | 42 | ### 死亡(Dead) 43 | * 线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。 44 | * 死亡的线程不可再次复生。在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。 45 | 46 | ## Java线程的相关方法 47 | ```java 48 | thread.run(); 49 | // 在当前线程开始运行 50 | thread.start(); 51 | // 在新的操作系统线程开始运行 52 | Thread.sleep(long ms); 53 | // 使当前线程进入阻塞,但不释放对象锁,一定时间后后线程自动苏醒进入Runnable状态。 54 | Thread.yield(); 55 | // 使当前线程放弃获取的CPU时间片,由Running变为Runnable状态,让OS再次选择线程。用来让相同优先级的线程轮流执行,但并不保证一定会轮流执行,因为让步之后还可能被线程调度程序再次选中。 56 | thread.join(); 57 | thread.join(long ms); 58 | // 当前线程阻塞,但不释放对象锁,直到线程thread执行完毕或者ms时间到,当前线程进入Runnable状态。 59 | thread.setDaemon(true); 60 | // 设置当前线程为thread的守护线程,意思是如果当前线程运行结束,则thread也会停止运行。 61 | thread.setPriority(Thread.MIN_PRIORITY); 62 | // 设置线程优先级,Thread类中有3个变量定义了线程优先级: 63 | // public final static int MIN_PRIORITY = 1; 64 | // public final static int NORM_PRIORITY = 5; 65 | // public final static int MAX_PRIORITY = 10; 66 | ``` 67 | 68 | ## Java线程同步 69 | Java使用共享内存的方式实现多线程之间的消息传递。因此,程序员需要写额外的代码用于线程之间的同步。 70 | 71 | ### synchronized方法和代码块 72 | * 把任意一个非null的对象当作锁。 73 | * 作用于非静态方法时,锁住的是对象的实例(this); 74 | * 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久代(jdk1.8中是metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程; 75 | * 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。 76 | * 既可以保证可见性,又能够保证原子性。 77 | * 可见性:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。 78 | * 原子性:要么不执行,要么执行到底。 79 | * synchronized不具有继承性,是一种高开销的操作,需要完成用户态到内核态的切换。 80 | * synchronized在JDK1.5中的优化: 81 | * 锁消除:即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。 82 | * 锁粗化:将多次连接在一起的加锁和解锁操作合并为一次,即将多个连续的锁改成一个大锁。 83 | * 自旋锁:当线程在获取轻量级锁的过程中执行CAS操作失败时,就通过自旋来获取重量级锁。 84 | * 适应性自旋:线程如果自旋获取锁成功了,则下次自旋的循环次数会更多;而如果自旋获取锁失败了,则下次自旋的次数就会减少。 85 | * 偏向锁、轻量级锁、重量级锁:见Java的锁机制——偏向锁/轻量级锁/重量级锁。 86 | 87 | ### volatile(用于类的成员变量) 88 | 89 | #### 可见性 90 | * 可见性就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。 91 | * 在Java中每条线程都有各自独立的存储空间,还有一个所有线程共享的内存空间。当新建线程时,系统会将共享内存中的所有共享变量拷贝一份到线程自己的存储空间中。接下来该线程在结束前的所有操作都是基于自己的存储空间进行的。因此若一条线程改变了一个共享变量,仅仅改变的是这条线程专属存储空间中的变量值,而此时如果其他线程访问这个变量,访问的还是修改前的值。 92 | * volatile修饰了一个成员变量后,这个变量的读写就会比普通变量多一些步骤: 93 | * volatile读:当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。 94 | * volatile写:当被volatile修饰的变量进行写操作时,这个变量将会被直接写入共享内存,而非线程的专属存储空间。 95 | * volatile的附送功能 96 | * 进行volatile写操作时,不仅会将volatile变量写入共享内存,系统还会将当前线程专属空间中的所有共享变量写入共享内存。 97 | * 进行volatile读操作时,系统也会一次性将共享内存中所有共享变量读入线程专属空间。 98 | * 这就意味着,如果普通变量在volatile写操作之前被修改,那么其他线程在volatile读操作之后就能正确读到他们。 99 | 100 | #### 指令重排序和happens-before原则 101 | * 指令重排包括编译期重排和运行期重排,是指处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 102 | * 若两行之间的某个变量被volatile修饰后,重排序规则会发生变化。在以下情况下,即使两行代码之间没有依赖关系,也不会发生重排序: 103 | * volatile读 104 | * 若volatile读操作的前一行为volatile读/写,则这两行不会发生重排序。 105 | * volatile读操作和它后一行代码都不会发生重排序。 106 | * volatile写 107 | * volatile写操作和它前一行代码都不会发生重排序。 108 | * 若volatile写操作的后一行代码为volatile读/写,则这两行不会发生重排序。 109 | 110 | #### happens-before 111 | * 在真实环境下,动作A和动作B的执行顺序是可以通过指令重排而发生变化的,但是你需要保证A和B的可见性,此时用户可以用满足happens-before原则的操作来规避指令重排: 112 | * happens-before定义: 113 | * 如果操作A happens-before 操作B,那么操作A的执行顺序排在操作B之前,而且操作A的执行结果将对操作B可见。 114 | * 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行,只要重排序之后的执行结果与按照happens-before关系来执行的结果一致就行。 115 | * happens-before具体规则: 116 | * 程序次序规则:一个线程内,按照代码顺序,写在前面的操作happens-before写在后面的操作; 117 | * 锁定规则:一个unlock操作happens-before后面对同一个锁的lock操作; 118 | * volatile变量规则:对一个volatile变量的写操作happens-before后面对这个变量的读操作; 119 | * 传递规则:如果操作A volatile 操作B,而操作B volatile 操作C,则可以得出操作A volatile 操作C; 120 | * 线程启动规则:Thread对象t的t.start()方法happens-before此线程的每个动作; 121 | * 线程中断规则:对线程对象的interrupt()方法的调用happens-before该线程的代码检测到中断事件的发生; 122 | * 线程终结规则:Thread的线程对象t中所有的操作都先行发生于t的终止检测,包括t.join()方法返回、t.isAlive()==false等手段可以检测到t的终止; 123 | * 对象终结规则:一个对象的初始化完成happens-before它的finalize()方法的开始。 124 | 125 | ### 原子变量和原子操作 126 | * 原子性指的是一组操作必须一起完成,中途不能被中断。 127 | * Java中的原子操作包括: 128 | * 对基本变量类型(除了double和long)的赋值。 129 | * 对引用的赋值。 130 | * 对java.concurrent.Atomic*所有类的操作。 131 | * 对volatile的long和double变量的赋值。(内部synchronized) 132 | * 读写long和double变量不是原子的,而是需要分成两步(前32位和后32位)。如果一个线程正在修改该long或double变量的值,另一个线程可能只能看到该值的一半(前32位)。为了避免这种情况,需要在用volatile修饰long、double型成员变量。 133 | 134 | #### UnSafe类 135 | * Unsafe类能直接原子性的、从硬件级别访问底层操作系统,但实际编码时是不能用的,否则会报异常。 136 | * Unsafe类的功能: 137 | * 对象实例化:一般我们用new或者反射来实例化对象,Unsafe使用`allocateInstance()`方法可以直接生成对象实例,并且无需调用构造方法和其它初始化方法。 138 | * 操作类、对象、变量:包括`staticFieldOffset`(静态域偏移)、`defineClass`(定义类)、`defineAnonymousClass`(定义匿名类)、`ensureClassInitialized`(确保类初始化)、`objectFieldOffset`(对象域偏移)等方法,通过这些方法我们可以获取对象的指针,通过对指针进行偏移,我们不仅可以直接修改指针指向的数据(即使它们是私有的),甚至可以找到JVM已经认定为垃圾、可以进行回收的对象。 139 | * 操作数组:包括`arrayBaseOffset`(获取数组第一个元素的偏移地址)和`arrayIndexScale`(获取数组中元素的增量地址),二者配合使用就可以定位数组中每个元素在内存中的位置。 140 | * CAS操作:Compare And Swap,比较替换操作是指令级别的操作,它为Java的锁机制提供了一种新的解决办法,比如AtomicXXX等类都是通过该方法来实现的。 141 | * 线程挂起与恢复:通过`park`方法实现将一个线程进行挂起,调用后,线程将一直阻塞直到超时或者中断等条件出现。`unpark`可以终止一个挂起的线程,让其恢复正常。 142 | 143 | #### AtomicXXX类 144 | * 提供了一些原子性的操作,可以避免使用高代价的synchronized。 145 | * `set()`是原子性地修改value值;而`lazySet()`修改的值不会对其他线程立即可见,可以减少不必要的内存屏障,从而提高程序执行的效率。 146 | * `getAndSet()`是原子性地返回旧值,设置新值;`compareAndSet()`是原子性地比较,一致时替换并返回true,不一致时返回false。 147 | * CAS操作常常与自旋锁一起使用,是一种高效率的无锁算法。 148 | 149 | ### Java的锁机制 150 | 151 | #### 可重入锁 152 | * 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁之后,进入内层方法时也会自动获取锁,好处是可以一定程度上避免死锁。 153 | * ReentrantLock是一个可重入锁,它内部维护了一个计数器,加锁一次计数器+1,加多少次锁就要解对应次数的锁。 154 | * synchronized也是一个可重入锁。 155 | * 以下代码是一个可重入锁的一个特点,如果不是可重入锁的话,taskB可能不会被当前线程执行,可能造成死锁。 156 | 157 | ```java 158 | synchronized void taskA() throws Exception { 159 | Thread.sleep(1000); 160 | taskB(); 161 | } 162 | synchronized void taskB() throws Exception { 163 | Thread.sleep(1000); 164 | } 165 | ``` 166 | 167 | * synchronized和ReentrantLock的区别: 168 | * ReentrantLock相对于synchronized有如下特点:可响应中断、可设置限时、公平锁。 169 | * synchronized适用于资源竞争不激烈的场景,而ReentrantLock适用于资源竞争激烈的场景。 170 | * synchronized是JVM层面实现的,而ReentrantLock是代码层面实现的。 171 | * 尽可能使用synchronized而不是ReentrantLock,原因是:简单易懂、编译器自动优化(适应性自旋、锁消除等)。 172 | 173 | #### 独享锁/共享锁 174 | * 独享锁是指该锁同时只能被一个线程所持有。 175 | * 共享锁是指该锁可被多个线程所持有。 176 | * Java的ReentrantLock和synchronized都是独享锁。但是Lock的另一个实现类ReadWriteLock的读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读是非常高效的。 177 | * 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。 178 | 179 | #### 乐观锁/悲观锁 180 | * 乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。 181 | * 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。 182 | * 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。 183 | * 悲观锁适合写操作多的场景,乐观锁适合读操作多的场景。 184 | * 悲观锁在Java中的使用,就是利用各种锁;乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS实现原子操作的更新。 185 | 186 | #### 自旋锁 187 | * 如果持有锁的线程能在很短时间内释放锁资源,那么尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。 188 | * 好处:减少线程上下文(内核态和用户态)切换的消耗。 189 | * 缺点:循环会消耗CPU。 190 | 191 | #### 公平锁/非公平锁 192 | * 公平锁是指多个线程按照申请锁的顺序来获取锁。 193 | * 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。这样有可能会造成优先级反转或者饥饿现象。 194 | * 对于Java的ReentrantLock,可以通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 195 | * synchronized也是一种非公平锁,由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。 196 | 197 | #### 偏向锁/轻量级锁/重量级锁 198 | * 这三种锁是指锁的状态,并且是针对synchronized的。在Java1.5通过引入锁升级的机制来实现高效的synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。 199 | * 偏向锁是指:偏向于第一个访问锁的线程。如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。而如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。 200 | * 轻量级锁是指:当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 201 | * 重量级锁是指:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 202 | 203 | ### wait()/notify() 204 | ```java 205 | object.wait(); 206 | object.wait(long t); 207 | // 当前线程释放对象锁,进入等待队列。依靠其他线程调用该对象的notify()/notifyAll()唤醒或者t时间到自动唤醒。 208 | object.notify(); 209 | // 唤醒在此对象监视器上等待的单个线程,选择是任意性的。 210 | object.notifyAll(); 211 | // 唤醒在此对象监视器上等待的所有线程。 212 | ``` 213 | wait()和notify()必须在synchronized的代码块中使用,因为只有在获取当前对象的锁时才能进行这两个操作,否则会报异常。 214 | 215 | ### Condition 216 | * Condition与ReentrantLock的关系就类似于synchronized与obj.wait()/obj.signal()。 217 | * con.await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用con.signal()时或者con.signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。 218 | * con.awaitUninterruptibly()方法与con.await()方法基本相同,但是它并不会再等待过程中响应中断。 219 | * con.singal()方法用于唤醒一个在等待中的线程,con.singalAll()方法会唤醒所有在等待中的线程。 220 | 221 | ### ThreadLocal 222 | * 每一个使用ThreadLocal变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。 223 | * 每个线程Thread中都有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,用来存储本线程中的变量副本,键值为ThreadLocal变量,value为变量副本(即T类型的变量)。 224 | * 初始时,Thread里的threadLocals为null,当通过ThreadLocal变量调用get()方法或者set()方法时,会初始化Thread类中的threadLocals,再执行相应操作。 225 | * ThreadLocalMap的Entry继承了WeakReference,是一个弱引用,在每次GC中都会回收,所以ThreadLocalMap实际上是一个WeakHashMap。 226 | * 在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 227 | 228 | ### BlockingQueue 229 | ```java 230 | LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); 231 | // 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue。 232 | queue.put(T t); 233 | // 在队尾添加一个元素,如果队列满则阻塞,类似生产者线程。 234 | queue.take(); 235 | // 移除并返回队头元素,如果队列空则阻塞,类似消费者线程。 236 | queue.size(); 237 | // 返回队列中的元素个数。 238 | ``` 239 | 240 | ### PipedInputStream/PipedOutputStream 241 | * 这个类位于java.io包中,是解决同步问题的最简单的办法。 242 | * 一个线程将数据写入管道,另一个线程从管道读取数据,这样便构成了一种生产者/消费者的缓冲区编程模式。 243 | * 只能用于多线程模式,用于单线程下可能会引发死锁。 244 | 245 | ### Semaphore 246 | ```java 247 | Semaphore s = new Semaphore(5); 248 | // 创建一个初始值为5的信号量 249 | s.acquire(); 250 | // 申请使用信号量,如果s的值大于0则允许使用,否则阻塞等待其他线程release()。 251 | s.release(); 252 | // 释放信号量资源,信号量的值+1,如果有等待的线程则通知它。 253 | ``` 254 | 255 | ## Java线程池 256 | 257 | ### Executor接口和ThreadPoolExecutor 258 | * 在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类。对线程池的配置,就是对ThreadPoolExecutor构造函数的参数的配置。 259 | 260 | #### ThreadPoolExecutor构造函数的参数 261 | * `int corePoolSize`:核心线程数最大值; 262 | * `int maximumPoolSize`:线程总数最大值; 263 | * `long keepAliveTime`:非核心线程闲置最大时限; 264 | * `TimeUnit unit`:keepAliveTime的时间单位,包括天、小时、分、秒、毫秒、微秒、纳秒; 265 | * `BlockingQueue workQueue`:任务队列,维护等待执行的Runnable对象; 266 | * `ThreadFactory threadFactory`:线程工厂,用来定义创建线程的方式,一般用不到; 267 | * `RejectedExecutionHandler handler`:表示当拒绝处理任务时的策略,有以下四种取值: 268 | * AbortPolicy:如果不能接受任务了,则抛出异常。 269 | * CallerRunsPolicy:如果不能接受任务了,则让调用的线程去完成。 270 | * DiscardOldestPolicy:如果不能接受任务了,则丢弃最老的一个任务,由一个队列来维护。 271 | * DiscardPolicy:如果不能接受任务了,则丢弃任务。 272 | 273 | #### 线程池状态 274 | * 在ThreadPoolExecutor中定义了一个volatile变量`runState`来表示线程池当前的状态,包括`RUNNING`、`SHUTDOWN`、`STOP`和`TERMINATED`。 275 | * `RUNNING`:创建线程池后,线程池处于RUNNING状态; 276 | * `SHUTDOWN`:调用线程池的`shutdown()`方法后,线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕; 277 | * `STOP`:调用线程池的`shutdownNow()`方法后,线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务; 278 | * `TERMINATED`:当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池就处于TERMINATED状态。 279 | 280 | #### 任务的执行 281 | * 向ThreadPoolExecutor添加任务:通过`ThreadPoolExecutor.execute(Runnable command)`方法即可向线程池内添加一个任务。 282 | * ThreadPoolExecutor的执行策略:当一个任务被添加进线程池,分为两种情况: 283 | * 线程数量未达到corePoolSize,则新建一个核心线程执行任务。 284 | * 线程数量达到了corePoolSize,则将任务移入队列等待,分为两种情况: 285 | * 队列已满,总线程数未达到maximumPoolSize,则新建非核心线程执行任务。 286 | * 队列已满,总线程数达到了maximumPoolSize,就会抛出异常。 287 | 288 | #### 核心线程 289 | * 线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程。 290 | * 核心线程默认情况下会一直存活在线程池中,即使它一直闲置。但如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程在闲置超过一定时间就会被销毁掉。 291 | 292 | #### workQueue等待队列的类型 293 | * SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,但如果所有线程都在工作时,会新建一个线程来处理这个任务。所以为了避免线程数达到了maximumPoolSize而不能新建线程的错误,使用这个类型队列时,maximumPoolSize一般指定成Integer.MAX_VALUE。 294 | * LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建一个核心线程处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize。 295 | * ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建一个核心线程执行任务,如果达到了,则入队等候,如果队列已满,则新建一个非核心线程执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误。 296 | * DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。 297 | 298 | #### 线程池的回调接口 299 | * 可以通过重写ThreadPoolExecutor的子类中的以下三个方法: 300 | * `beforeExecute(Thread t, Runnable r);` 301 | * `afterExecute(Runnable r, Throwable t);` 302 | * `terminated();` 303 | * 来实现在线程执行前后,包括抛出异常时,还有线程池退出时的日志管理或其他操作。 304 | 305 | ### 常见的四种线程池 306 | 307 | #### CachedThreadPool:可缓存线程池(推荐使用) 308 | 当线程池大小超过了处理任务所需的线程,就会回收部分空闲(一般是60秒无执行)的线程;当有任务来时,也能添加新线程来执行。 309 | 310 | ```java 311 | // 创建方法 312 | ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); 313 | 314 | // 源码 315 | public static ExecutorService newCachedThreadPool() { 316 | return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); 317 | } 318 | ``` 319 | 320 | #### FixedThreadPool:定长线程池 321 | 可控制线程最大并发数(同时执行的线程数),超出的线程会在队列中等待。 322 | 323 | ```java 324 | // 两种创建方法 325 | // nThreads:最大线程数即maximumPoolSize 326 | ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads); 327 | // threadFactory:创建线程的方法 328 | ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory); 329 | 330 | // 源码 331 | public static ExecutorService newFixedThreadPool(int nThreads) { 332 | return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); 333 | } 334 | ``` 335 | 336 | #### ScheduledThreadPool:周期线程池 337 | 支持定时及周期性任务的执行,用于执行计划任务。 338 | 339 | ```java 340 | // 创建方法 341 | // nThreads:最大线程数即maximumPoolSize 342 | ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize); 343 | 344 | // 源码 345 | public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { 346 | return new ScheduledThreadPoolExecutor(corePoolSize); 347 | } 348 | public ScheduledThreadPoolExecutor(int corePoolSize) { 349 | super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); 350 | } 351 | ``` 352 | 353 | #### SingleThreadExecutor:单线程化的线程池 354 | 只有一个工作线程执行任务,所有任务按照指定顺序执行,即遵循队列的入队出队规则。 355 | 356 | ```java 357 | // 创建方法 358 | ExecutorService singleThreadPool = Executors.newSingleThreadPool(); 359 | 360 | // 源码 361 | public static ExecutorService newSingleThreadExecutor() { 362 | return new FinalizableDelegatedExecutorService 363 | (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())); 364 | } 365 | ``` 366 | 367 | -------------------------------------------------------------------------------- /Java/JVM.md: -------------------------------------------------------------------------------- 1 | # JVM 2 | 3 | ## JVM内存模型 4 | 5 | ### 程序计数器(Program Counter Register) 6 | * 是一块线程私有的较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 7 | * 程序计数器是唯一一个在JVM中没有规定任何OutOfMemoryError情况的区域。 8 | 9 | ### 线程栈(Java Virtual Machine Stack,JVM栈) 10 | * 每个线程都有一个线程私有的栈,它的生命周期与线程相同。 11 | * 线程中每个方法执行时会压入一个栈帧(Stack Frame),栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 12 | * 局部变量表中存放的数据是基本数据类型、对象引用和返回地址(指向一条字节码指令的地址)。基本数据类型中,64位长度的long和double会占用2个Slot,其余数据类型只占用1个Slot。局部变量表所需的内存空间在编译期间完成分配。 13 | * 如果线程请求的栈深度大于JVM允许的深度,将抛出StackOverFlowError异常;如果线程栈可以动态扩展,但扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。 14 | 15 | ### 本地方法栈(Native Method Stack) 16 | * 与线程栈作用相似,区别在于线程栈为Java方法(字节码)服务,而本地方法栈则为Native方法服务。 17 | * 没有强制规定具体实现,可以把线程栈和本地方法栈合二为一。 18 | * 与线程栈一样,本地方法栈也会抛出StackOverFlowError异常和OutOfMemoryError异常。 19 | 20 | ### Java堆(Java Heap) 21 | * JVM所管理的内存中最大的一块,被所有线程共享的内存区域,在虚拟机启动时创建。 22 | * Java堆用来存放对象示例,几乎所有的对象示例和数组都在这里分配内存。 23 | * 不是所有的对象都在堆里分配内存,是因为逃逸分析、栈上分配、标量替换等技术发展之后,有些对象可以放在栈中。 24 | * Java堆是GC管理的主要区域,所以Java堆也被称为GC堆。 25 | * Java堆可以处于物理不连续的内存空间中,只要逻辑上连续即可。 26 | * 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。 27 | 28 | ### 方法区(Method Area) 29 | * 又称为永久代(Perm Generation),是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 30 | * 方法区除了和Java堆一样不需要连续的物理内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集。 31 | * 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。 32 | 33 | ### 运行时常量池(Runtime Constant Pool) 34 | * 是方法区的一部分,用于存放编译期生成的各种字面量、符号引用和翻译出来的直接引用,这部分内容将在类加载后存放于运行时常量池。 35 | * 运行时常量池相对于Class文件常量池的另外一个重要特征是动态性,即常量不一定只有编译器产生,运行时也可以把新的常量放入池中,比如String.intern()方法。 36 | * 当常量池无法再申请到内存时会抛出OutOfMemoryError异常。 37 | 38 | ### 直接内存(Direct Memory) 39 | * 并不是虚拟机运行时数据区的一部分,也叫堆外内存。 40 | * 配置虚拟机内存大小的参数(-Xmx)时不能忽略,因为当各个内存区域总和大于物理内存的限制时,会抛出OutOfMemoryError。 41 | 42 | ## String.intern()方法详解 43 | * new String()是在堆上创建字符串对象,当调用intern()方法时,编译器会将字符串添加到常量池中,并返回指向该常量的引用。 44 | * 通过字面量赋值创建字符串时,如`String str="JAVA";`,会先在常量池中查找是否存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。 45 | * 常量字符串的"+"操作,编译阶段直接会合成为一个字符串。如`String str="JA"+"VA";`在编译阶段会直接合并成`String str="JAVA";`。 46 | * 对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。如:`final String str1="JA"; final String str2="VA"; String str3=str1+str2; `这段代码在编译时,直接替换成了`String str3="JA"+"VA";`然后根据上一条规则,再次替换成`String str3="JAVA"`。 47 | * 常量字符串和变量拼接时,如`String str3=baseStr+"01";`,会调用`StringBuilder.append()`在堆上创建新的对象。 48 | * JDK1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。 49 | 50 | ## 创建对象 51 | 52 | ### 对象的创建过程 53 | 1. 检查常量池中是否有即将要创建的这个对象所属的类的符号引用,如果没有则说明这个类还没有被定义,抛出ClassNotFoundException。 54 | 2. 检查这个符号引用所代表的类是否已经被JVM加载,如果该类没有被加载就找该类的class文件,并加载该类进方法区。 55 | 3. 根据方法区中该类的信息确定该类所需的内存大小,然后从堆中划分一块对应大小的内存空间给新的对象。分配堆中内存有两种方式: 56 | * 指针碰撞:如果堆中空闲内存是完整的区域,并且空闲内存和已使用内存之间由一个指针标记。那么当为一个对象分配内存时,只需移动指针即可。因此,这种在完整空闲区域上通过移动指针来分配内存的方式就叫做"指针碰撞"。 57 | * 空闲列表:如果堆中空闲区域和已使用区域交错,因此需要用一张"空闲列表"来记录堆中哪些区域是空闲区域,从而在创建对象的时候根据这张"空闲列表"找到空闲区域,并分配内存。 58 | 4. 为对象中的成员变量赋上初始值,即默认初始化。 59 | 5. 设置对象头中的信息,创建对象完成。 60 | 61 | ### 对象的内存模型 62 | 对象在内存中分为三个部分:对象头、实例数据、对齐补充。 63 | 64 | #### 对象头 65 | 对象头中记录了对象在运行过程中所需要使用的一些数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。此外对象头中可能还包含类型指针,通过该指针能确定这个对象所属哪个类。如果对象是一个数组,那么对象头中还要包含数组长度。 66 | 67 | #### 实例数据 68 | 实例数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。 69 | 70 | #### 对齐补充 71 | 用于确保对象的总长度为8字节的整数倍。HotSpot要求对象的总长度必须是8字节的整数倍。由于对象头一定是8字节的整数倍,但实例数据部分的长度是任意的,因此需要对齐补充字段确保整个对象的总长度为8的整数倍。 72 | 73 | ### 访问对象的方法 74 | 引用类型的变量中存放的是一个地址,那么根据地址类型的不同,对象有不同的访问方式。 75 | 76 | #### 句柄访问方式 77 | * 堆中需要有一块叫做"句柄池"的内存空间,用于存放所有对象的地址和所有对象所属类的类信息。 78 | * 引用类型的变量存放的是该对象在句柄池中的地址。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址再访问对象。 79 | 80 | #### 直接指针访问方式 81 | * 引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。 82 | * 但对象所在的内存空间中需要额外的策略存储对象所属的类信息的地址。 83 | 84 | ## 垃圾收集器(Garbage Collector,GC) 85 | System.gc()和Runtime.gc()用来提示JVM要进行垃圾回收。但是,立即开始还是延迟进行垃圾回收是取决于JVM的。 86 | 87 | ### Java堆内存 88 | 89 | #### 新生代(Young Generation) 90 | * 新生代由一个Eden与两个Survivor Space(From和To)构成,大小通过-Xmn参数指定,Eden与Survivor Space的内存大小比例默认为8:1:1,可以通过-XX:SurvivorRatio参数指定。 91 | * Eden:大多数情况下,新对象都在Eden中分配,当Eden没有足够空间时,会触发一次Minor GC。 92 | * Survivor:当新生代发生GC(Minor GC)时,会将Eden和From中存活的对象移动到To,并清空Eden区域,然后From和To交换角色,再次发生Minor GC时重复该过程。因此存活对象会反复在两个Survivor Space之间移动,移动时对象的GC年龄自动累加,当GC年龄超过默认阈值15时,会将该对象移动到老年代,可以通过参数-XX:MaxTenuringThreshold对GC年龄的阈值进行设置。 93 | * 动态对象年龄判定:如果在Suvivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。 94 | 95 | #### 空间分配担保 96 | * 在发生Minor GC时,虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和,若成立,则说明Minor GC是安全的,否则,虚拟机需要查看HandlePromotionFailure的值,看是否允许担保失败,若允许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次Minor GC;若小于或者HandlePromotionFailure设置不运行冒险,那么此时将改成一次Full GC。 97 | * 其中冒险是指经过一次Minor GC后有大量对象存活,而新生代的Survivor区放不下这些存活的对象,所以需要老年代进行分配担保,把Survivor区无法容纳的对象直接进入老年代。 98 | * 以上是JDK Update 24之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。 99 | 100 | #### 老年代(Old Generation) 101 | * 老年代用来存放大对象(需要很大连续空间的对象,如很长的字符串和数组)和经过几次Minor GC之后依旧存活的对象,空间大小就是-Xmx与-Xmn两个参数之差。 102 | * 当老年代的空间不足时,会触发Major GC/Full GC,速度一般比Minor GC慢10倍以上。 103 | 104 | #### 永久代(Permanent Generation) 105 | * JDK8以前,类的元数据如方法数据、方法信息(字节码,栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32位默认永久代的大小为64M,64位默认为85M,可以通过参数-XX:MaxPermSize进行设置,一旦类的元数据超过了永久代大小,就会抛出OOM异常。 106 | * 在JDK8的HotSpot中,把永久代从Java堆中移除了,并把类的元数据直接保存在本地内存区域(堆外内存),称之为元空间。这样做的好处:对永久代的调优过程非常困难,永久代的大小很难确定,其中涉及到太多因素,如类的总数、常量池大小和方法数量等,而且永久代的数据可能会随着每一次Full GC而发生移动。而在JDK8中,类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间,可以避免永久代的内存溢出问题,不过需要监控内存的消耗情况,一旦发生内存泄漏,会占用大量的本地内存。 107 | * 注意:JDK7之前的HotSpot,字符串常量池的字符串被存储在永久代中,因此可能导致一系列的性能问题和内存溢出错误。在JDK8中,字符串常量池中只保存字符串的引用。 108 | 109 | ### 判断对象存活的方法 110 | GC动作发生之前,需要确定堆内存中哪些对象是存活的,一般有两种方法:引用计数法和可达性分析法。Java主要用的是可达性分析法。 111 | 112 | #### 引用计数法 113 | * 在对象里添加一个引用计数器,每当有一个地方引用该对象时,计数器值就加1;当该引用失效时,计数器值就减1。任何时刻计数器值为0的对象表示不可能再被使用。 114 | * 引用计数法实现简单,判定高效,但不能解决对象之间相互引用的问题。 115 | 116 | #### 可达性分析法 117 | * 通过一系列称为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(图论中称为不可达),意味着该对象可以被回收。 118 | * 以下对象可作为GC Roots: 119 | * 本地变量表中引用的对象 120 | * 方法区中静态变量引用的对象 121 | * 方法区中常量引用的对象 122 | * Native方法引用的对象 123 | * 在可达性分析法中,判定一个对象是否可回收,至少要经历两次标记过程: 124 | 1. 如果该对象到GC Roots没有引用链,则进行第一次标记。 125 | 2. 如果该对象重写了finalize()方法,且还未执行过,那么它会被插入到F-Queue队列中,然后由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法。finalize()方法是对象逃脱死亡的最后机会,GC会对队列中的对象进行第二次标记,如果该对象在finalize()方法中与引用链上的任何一个对象建立联系,那么它就会被移出"即将回收"的集合。(当然,在实际项目中应该尽量避免使用finalize方法) 126 | 127 | ### Java的四种引用 128 | * 强引用:new出的对象之类的引用,只要强引用还在,宁愿抛OOM异常也不会回收。 129 | * 软引用(SoftReference):内存溢出异常之前回收,多用于缓存场景。 130 | * 弱引用(WeakReference):GC发现了就回收,常用于HashMap或HashSet中key为对象时,应该使用WeakHashMap和WeakHashSet。 131 | * 虚引用(PhantomReference):也是GC发现了就回收,但在回收前会放入ReferenceQueue,而其他引用都是回收后才放入的,虚引用常用于引用销毁前的处理工作,一般很少用到。 132 | 133 | ### 回收方法区(永久代) 134 | * 方法区的垃圾收集主要包括两部分内容:废弃常量和无用的类。 135 | * 废弃常量:回收废弃常量与回收Java堆中的对象非常相似。以常量池中字面量的回收为例,若字符串"abc"已经进入常量池中,但当前系统没有任何String对象引用常量池中的"abc"常量,也没有其他地方引用该字面量,若发生内存回收,且必要的话,该"abc"就会被系统清理出常量池。常量池中其他的类(接口)、方法、字段的符号引用与此类似。 136 | * 无用的类:满足以下3个条件就会被回收: 137 | * 该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例; 138 | * 加载该类的ClassLoader已经被回收; 139 | * 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 140 | 141 | ### 垃圾收集算法 142 | 143 | #### 标记-清除算法(Mark-Sweep) 144 | * 分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。 145 | * 是最基础的收集算法,因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。 146 | * 缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 147 | 148 | #### 复制算法(Copying) 149 | * 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 150 | * 这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。 151 | * 这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。 152 | 153 | #### 标记-整理算法(Mark-Compact) 154 | * 复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 155 | * 根据老年代的特点,有人提出了"标记-整理"算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 156 | 157 | #### 分代收集算法 158 | * 把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 159 | * 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 160 | * 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记-清理"或"标记-整理"算法来进行回收。 161 | 162 | ### 对象标记过程 163 | 在可达性分析过程中,为了准确找出与GC Roots相关联的对象,必须要求整个执行引擎看起来像是被冻结在某个时间点上,即暂停所有运行中的线程,不可以出现对象的引用关系还在不断变化的情况。 164 | 165 | #### 如何快速枚举GC Roots? 166 | * GC Roots主要在全局性的引用(常量或类静态属性)与执行上下文(本地变量表中的引用)中,很多应用仅仅方法区就上百兆,如果进行遍历查找,效率会非常低下。 167 | * 在HotSpot中,使用一组称为OopMap的数据结构进行实现。类加载完成时,HotSpot把对象内什么偏移量上是什么类型的数据计算出来存储到OopMap中,通过JIT编译出来的本地代码,也会记录下栈和寄存器中哪些位置是引用。GC发生时,通过扫描OopMap的数据就可以快速标识出存活的对象。 168 | 169 | #### 如何进行安全的GC? 170 | * 线程运行时,只有在到达安全点(Safe Point)才能停顿下来进行GC。 171 | * 基于OopMap数据结构,HotSpot可以快速完成GC Roots的遍历,不过HotSpot并不会为每条指令都生成对应的OopMap,只会在Safe Point处记录这些信息。 172 | * 所以Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。 173 | 174 | #### 发生GC时,如何让所有线程跑到最近的Safe Point再暂停? 175 | * 当发生GC时,不直接对线程进行中断操作,而是简单的设置一个中断标志,每个线程运行到Safe Point的时候,主动去轮询这个中断标志,如果中断标志为真,则将自己进行中断挂起。 176 | * 这里忽略了一个问题,当发生GC时,运行中的线程可以跑到Safe Point后进行挂起,而那些处于Sleep或Blocked状态的线程在此时无法响应JVM的中断请求,无法到Safe Point处进行挂起,针对这种情况,可以使用安全区域(Safe Region)进行解决。 177 | * Safe Region是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。 178 | * 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程; 179 | * 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止。 180 | 181 | ### 垃圾收集器 182 | 如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。 183 | 184 | #### Serial收集器 & Serial Old收集器 185 | * 采用单个线程的收集器,进行垃圾收集时,必须暂停其他所有的工作线程(Stop The World)。 186 | * 在新生代采用复制算法(Serial收集器),在老年代采用标记-整理算法(Serial Old收集器)。 187 | * 对于单CPU环境来说,Serial由于没有线程交互的开销,可以简单而高效的进行垃圾收集动作,是Client模式下新生代默认的收集器。 188 | 189 | #### ParNew 收集器 190 | * 其实就是Serial收集器的多线程版本,除此之外和Serial一样。 191 | * 在新生代采用复制算法,在老年代采用标记-整理算法。 192 | * 是许多Server模式下的首选的新生代收集器,因为除了Serial收集器外,只有它能和CMS收集器配合工作。 193 | 194 | #### Parallel Scavenge收集器 & Parallel Old收集器 195 | * 类似ParNew收集器,但更关注吞吐量,称为"吞吐量优先"的收集器。 196 | * 吞吐量 = 用户代码运行时间 /(用户代码运行时间 + 垃圾收集时间) 197 | * 可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例。 198 | * 在新生代采用复制算法(Parallel Scavenge收集器),在老年代采用标记-整理算法(Parallel Old收集器)。 199 | 200 | #### CMS 收集器(Concurrent Mark Sweep) 201 | * 以获取最短回收停顿时间为目标,是一个使用"标记-清除"算法实现的老年代收集器。 202 | * CMS的回收过程分为4步: 203 | 1. 初始标记(initial mark):需要Stop The World,仅仅标记GC Roots能够直接关联到的对象,速度很快; 204 | 2. 并发标记(concurrent mark):进行GC Roots Tracing的过程,就是从GC Roots开始往下追溯,和用户线程并发执行; 205 | 3. 重新标记(remark):用于修正并发标记期间由于用户程序继续运行而导致标记产生变动的那部分记录,需要Stop The World,但停顿时间远比并发标记的时间短; 206 | 4. 并发清理(concurrent sweep):可以和用户线程一起工作。 207 | * CMS收集器的缺点: 208 | * 基于标记-清除算法实现的,意味着收集结束后会造成大量的内存碎片,可能导致出现老年代剩余空间很大,却无法找到足够大的连续空间分配当前对象,不得不提前触发一次Full GC。 209 | * 对CPU资源比较敏感,在并发阶段,虽然不会导致用户线程停顿,但是会占用一部分线程资源,降低系统的总吞吐量。 210 | * 无法处理浮动垃圾,即并发清理阶段其他用户线程运行时出现的垃圾,只能下一次GC再清理。 211 | * 如果CMS运行期间预留的内存无法满足用户线程需要,会出现一次"Concurrent Mode Failure"失败,这时虚拟机会启动Serial Old收集器对老年代进行垃圾收集,这样停顿时间会很长。 212 | 213 | #### G1 收集器(Garbage-First) 214 | * 与CMS收集器相比,G1收集器有以下优点: 215 | * 并行与并发:G1能充分利用多个CPU来缩短Stop The World的停顿时间。 216 | * 分代收集:不需要其他收集配合就可以管理整个Java堆,采用不同的方式处理新建的对象、已经存活一段时间和经历过多次GC的对象获取更好的收集效果。 217 | * 空间整合:G1收集器采用标记-整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。 218 | * 预测停顿:这是G1的另一大优势,可以建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。 219 | * 内存分区(Region)思路: 220 | * G1收集器将整个Java堆划分为多个大小相等的Region,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。 221 | * G1会跟踪各个Region的垃圾收集情况(回收空间大小和回收消耗的时间),维护一个优先列表,根据允许的收集时间,优先回收价值最大的Region,避免在整个Java堆上进行全区域的垃圾回收,确保了G1收集器可以在有限的时间内尽可能收集更多的垃圾。 222 | * 回收时以Region为单位进行回收,存活的对象复制到另一个空闲Region中。 223 | * 不过问题来了:使用G1收集器,一个对象分配在某个Region中,可以和Java堆上任意的对象有引用关系,那么如何判定一个对象是否存活,是否需要扫描整个Java堆?针对这种情况,虚拟机提供了一个解决方案:G1收集器中Region之间的对象引用关系和其他收集器中新生代与老年代之间的对象引用关系被保存在Remenbered Set数据结构中,用来避免全堆扫描。G1中每个Region都有一个对应的Remenbered Set,当虚拟机发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于相同的Region中,如果不是,则通过CardTable把相关引用信息记录到被引用对象所属Region的Remenbered Set中。 224 | 225 | ## Class类文件的结构 226 | 227 | ### 类文件简介 228 | * 每一个类文件都对应着一个类或接口的定义信息,但是类或接口并不一定都得定义在文件里,譬如类或接口也可以通过类加载器直接生成。 229 | * 每个类文件都是由以8字节为单位的字节流组成,所有的16位、32位和64位长度的数据将被构造成2个、4个和8个8字节单位来表示。多字节数据项总是按照大端存储的顺序进行存储。**注:大端存储顺序是指按高位字节在地址最低位,最低字节在地址最高位来存储数据。** 230 | * 在类文件中,各部分按照严格顺序连续存放,它们之间没有任何填充或对齐作为各项间的分隔符号。 231 | * 定义了一组私有数据类型来表示类文件的内容,它们包括u1、u2和u4,分别代表了1、2和4个字节的无符号数。 232 | 233 | ### 类文件内容 234 | | 类型 | 名称 | 数量 | 含义 | 235 | | --- | --- | --- | --- | 236 | | u4 | magic | 1 | 魔数 | 237 | | u2 | minor_version | 1 | 副版本号 | 238 | | u2 | major_version | 1 | 主版本号 | 239 | | u2 | constant_pool_count | 1 | 常量池计数器 | 240 | | cp_info | constant_pool | constant_pool_count-1 | 常量池 | 241 | | u2 | access_flags | 1 | 访问标志 | 242 | | u2 | this_class | 1 | 类索引 | 243 | | u2 | super_class | 1 | 父类索引 | 244 | | u2 | interfaces_count | 1 | 接口计数器 | 245 | | u2 | interfaces | interfaces_count | 接口表 | 246 | | u2 | fields_count | 1 | 字段计数器 | 247 | | field_info | fields | fields_count | 字段表 | 248 | | u2 | methods_count | 1 | 方法计数器 | 249 | | method_info | methods | methods_count | 方法表 | 250 | | u2 | attributes_count | 1 | 属性计数器 | 251 | | attribute_info | attributes | attributes_count | 属性表 | 252 | 253 | > 其中以"_info"结尾的类型是指:由多个无符号数或者其它表作为数据项构成的复合数据类型。 254 | 255 | * 魔数:固定为0xCAFEBABE不变,作用是确定该class文件是否为一个能被虚拟机所接受的类文件。 256 | * 副版本号和主版本号:共同构成了class文件的格式版本号。 257 | * 常量池计数器:等于常量池中常量的数量+1,常量池的索引范围是0的过程,虚拟机保证方法执行之前先执行父类的方法。这里的方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。**注:如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。** 320 | 321 | ### 类加载器和双亲委派模型 322 | 323 | #### 类加载器简介 324 | * 虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。 325 | * 类加载器用在加载阶段(Loading)。 326 | 327 | #### JVM的三种类加载器 328 | * 启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 329 | * 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext目录中的,或通过java.ext.dirs系统变量指定路径中的类。 330 | * 应用程序类加载器(Application ClassLoader):负责加载用户路径(CLASSPATH)上的类。 331 | 332 | > JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。 333 | 334 | #### 双亲委派模型 335 | ![双亲委派模型](media/JVM/双亲委派模型.jpg) 336 | 337 | * 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试自己执行加载任务。 338 | * 采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。 339 | 340 | ## JIT(Just In Time)编译器 341 | 342 | ### 概述 343 | * 在主流商用JVM(HotSpot、J9)中,Java程序一开始是通过解释器(Interpreter)进行解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会把这些代码认定为热点代码(Hot Spot Code),然后JVM会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为:即时编译器(Just In Time Compiler,JIT)。 344 | * JIT编译器是"动态编译器"的一种,相对的"静态编译器"则是指的比如:C/C++的编译器。 345 | * JIT并不是JVM的必须部分,JVM规范并没有规定JIT必须存在,更没有限定和指导JIT。但是,JIT性能的好坏、代码优化程度的高低却是衡量一款JVM是否优秀的最关键指标之一,也是虚拟机中最核心且最能体现虚拟机技术水平的部分。 346 | 347 | ### 编译对象 348 | * 编译对象就是之前说的"热点代码",它有两类: 349 | * 被多次调用的方法:一个方法被多次调用,理应称为热点代码,这种编译也是虚拟机中标准的JIT编译方式。 350 | * 被多次执行的循环体:编译动作由循环体出发,但编译对象依然会以整个方法为对象;这种编译方式由于编译发生在方法执行过程中,因此形象的称为:栈上替换(On Stack Replacement,OSR)编译,即方法栈帧还在栈上,方法就被替换了。 351 | 352 | ### 触发条件 353 | 判断一段代码是不是热点代码,是不是需要触发JIT编译,这样的行为称为:热点探测(Hot Spot Detection)。 354 | 355 | #### 主流的热点探测方式 356 | * 基于计数器的热点探测(Counter Based Hot Spot Detection):虚拟机会为每个方法(或每个代码块)建立计数器,统计执行次数,如果超过阀值那么就是热点代码。缺点是维护计数器开销。 357 | * 基于采样的热点探测(Sample Based Hot Spot Detection):虚拟机会周期性检查各个线程的栈顶,如果某个方法经常出现在栈顶,那么就是热点代码。缺点是不精确。 358 | * 基于踪迹的热点探测(Trace Based Hot Spot Detection):Dalvik中的JIT编译器使用这种方式。 359 | 360 | #### HotSpot使用的热点探测方式 361 | HotSpot使用的是第一种,基于计数器的热点探测,因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter): 362 | 363 | ##### 方法计数器 364 | * 默认阀值,在Client模式下是1500次,Server是10000次,可以通过参数"-XX:CompileThreshold"来设定。 365 | * 当一个方法被调用时会首先检查是否存在被JIT编译过的版本,如果存在则使用此本地代码来执行;如果不存在,则将方法计数器+1,然后判断"方法计数器和回边计数器之和"是否超过阀值,如果是则会向编译器提交一个方法编译请求。 366 | * 默认情况下,执行引擎并不会同步等待上面的编译完成,而是会继续解释执行。当编译完成后,此方法的调用入口地址会被系统自动改写为新的本地代码地址。 367 | * 还有一点,热度是会衰减的,也就是说不是仅仅+,也会-,热度衰减动作是在虚拟机的GC执行时顺便进行的。 368 | 369 | ##### 回边计数器 370 | * 回边,顾名思义,只有执行到大括号"}"时才算+1。 371 | * 默认阀值,Client下13995,Server下10700。 372 | * 它的调用逻辑和方法计数器差不多,只不过遇到回边指令时+1、超过阀值时会提交栈上替换OSR编译请求以及这里没有热度衰减。 373 | 374 | ### 优化技术 375 | 376 | #### 逃逸分析(Escape Analysis) 377 | * 逃逸分析不是具体的优化手段,而是作为类似标量替换等技术的前置分析技术。 378 | * 如果能证明别的线程或方法无法通过任何途径访问到一个对象,那么就说对象没有逃逸,可以进行标量替换。 379 | 380 | #### 标量替换(Scalar Replacement) 381 | * 不能分解的数据类型称为标量,就是Java中原始数据类型(包括引用),反之是聚合量(Java对象)。 382 | * 把一个对象拆散,将其成员变量恢复为原始数据类型来访问,就叫做标量替换。 383 | * 如果逃逸分析证明一个对象没有逃逸,且对象可以被拆散的话,那么程序执行时可能不在堆中创建这个对象,而直接改为在栈空间中创建成员变量让对象所在的方法来访问。 384 | 385 | --------------------------------------------------------------------------------