auto commit

This commit is contained in:
CyC2018 2018-03-25 21:47:21 +08:00
parent b590858f29
commit 23d3abb935
37 changed files with 24880 additions and 153 deletions

View File

@ -0,0 +1,742 @@
<!-- GFM-TOC -->
* [前言](#前言)
* [1. 小米-小米Git](#1-小米-小米git)
* [2. 小米-懂二进制](#2-小米-懂二进制)
* [3. 小米-中国牛市](#3-小米-中国牛市)
* [4. 微软-LUCKY STRING](#4-微软-lucky-string)
* [5. 微软-Numeric Keypad](#5-微软-numeric-keypad)
* [6. 微软-Spring Outing](#6-微软-spring-outing)
* [7. 微软-S-expression](#7-微软-s-expression)
* [8. 华为-最高分是多少](#8-华为-最高分是多少)
* [9. 华为-简单错误记录](#9-华为-简单错误记录)
* [10. 华为-扑克牌大小](#10-华为-扑克牌大小)
* [11. 去哪儿-二分查找](#11-去哪儿-二分查找)
* [12. 去哪儿-首个重复字符](#12-去哪儿-首个重复字符)
* [13. 去哪儿-寻找Coder](#13-去哪儿-寻找coder)
* [14. 美团-最大差值](#14-美团-最大差值)
* [15. 美团-棋子翻转](#15-美团-棋子翻转)
* [16. 美团-拜访](#16-美团-拜访)
* [17. 美团-直方图内最大矩形](#17-美团-直方图内最大矩形)
* [18. 美团-字符串计数](#18-美团-字符串计数)
* [19. 美团-平均年龄](#19-美团-平均年龄)
* [20. 百度-罪犯转移](#20-百度-罪犯转移)
* [22. 百度-裁减网格纸](#22-百度-裁减网格纸)
* [23. 百度-钓鱼比赛](#23-百度-钓鱼比赛)
* [24. 百度-蘑菇阵](#24-百度-蘑菇阵)
<!-- GFM-TOC -->
# 前言
省略的代码:
```java
import java.util.*;
```
```java
public class Solution {
}
```
```java
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
}
}
}
```
# 1. 小米-小米Git
- 重建多叉树
- 使用 LCA
```java
private class TreeNode {
int id;
List<TreeNode> childs = new ArrayList<>();
TreeNode(int id) {
this.id = id;
}
}
public int getSplitNode(String[] matrix, int indexA, int indexB) {
int n = matrix.length;
boolean[][] linked = new boolean[n][n]; // 重建邻接矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
linked[i][j] = matrix[i].charAt(j) == '1';
}
}
TreeNode tree = constructTree(linked, 0);
TreeNode ancestor = LCA(tree, new TreeNode(indexA), new TreeNode(indexB));
return ancestor.id;
}
private TreeNode constructTree(boolean[][] linked, int root) {
TreeNode tree = new TreeNode(root);
for (int i = 0; i < linked[root].length; i++) {
if (linked[root][i]) {
linked[i][root] = false; // 因为题目给的邻接矩阵是双向的,在这里需要把它转为单向的
tree.childs.add(constructTree(links, i));
}
}
return tree;
}
private TreeNode LCA(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root.id == p.id || root.id == q.id) return root;
TreeNode ancestor = null;
int cnt = 0;
for (int i = 0; i < root.childs.size(); i++) {
TreeNode tmp = LCA(root.childs.get(i), p, q);
if (tmp != null) {
ancestor = tmp;
cnt++;
}
}
return cnt == 2 ? root : ancestor;
}
```
# 2. 小米-懂二进制
对两个数进行异或,结果的二进制表示为 1 的那一位就是两个数不同的位。
```java
public int countBitDiff(int m, int n) {
return Integer.bitCount(m ^ n);
}
```
# 3. 小米-中国牛市
背包问题,可以设一个大小为 2 的背包。
状态转移方程如下:
```html
dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj]))
```
```java
public int calculateMax(int[] prices) {
int n = prices.length;
int[][] dp = new int[3][n];
for (int i = 1; i <= 2; i++) {
int localMax = dp[i - 1][0] - prices[0];
for (int j = 1; j < n; j++) {
dp[i][j] = Math.max(dp[i][j - 1], prices[j] + localMax);
localMax = Math.max(localMax, dp[i - 1][j] - prices[j]);
}
}
return dp[2][n - 1];
}
```
# 4. 微软-LUCKY STRING
- 斐波那契数列可以预计算;
- 从头到尾遍历字符串的过程,每一轮循环都使用一个 Set 来保存从 i 到 j 出现的字符,并且 Set 保证了字符都不同,因此 Set 的大小就是不同字符的个数。
```java
Set<Integer> fibSet = new HashSet<>(Arrays.asList(1, 2, 3, 5, 8, 13, 21, 34, 55, 89));
Scanner in = new Scanner(System.in);
String str = in.nextLine();
int n = str.length();
Set<String> ret = new HashSet<>();
for (int i = 0; i < n; i++) {
Set<Character> set = new HashSet<>();
for (int j = i; j < n; j++) {
set.add(str.charAt(j));
int cnt = set.size();
if (fibSet.contains(cnt)) {
ret.add(str.substring(i, j + 1));
}
}
}
String[] arr = ret.toArray(new String[ret.size()]);
Arrays.sort(arr);
for (String s : arr) {
System.out.println(s);
}
```
# 5. 微软-Numeric Keypad
```java
private static int[][] canReach = {
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 0
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, // 1
{1, 0, 1, 1, 0, 1, 1, 0, 1, 1}, // 2
{0, 0, 0, 1, 0, 0, 1, 0, 0, 1}, // 3
{1, 0, 0, 0, 1, 1, 1, 1, 1, 1}, // 4
{1, 0, 0, 0, 0, 1, 1, 0, 1, 1}, // 5
{0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, // 6
{1, 0, 0, 0, 0, 0, 0, 1, 1, 1}, // 7
{1, 0, 0, 0, 0, 0, 0, 0, 1, 1}, // 8
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1} // 9
};
private static boolean isLegal(char[] chars, int idx) {
if (idx >= chars.length || idx < 0) return true;
int cur = chars[idx] - '0';
int next = chars[idx + 1] - '0';
return canReach[cur][next] == 1;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int T = Integer.valueOf(in.nextLine());
for (int i = 0; i < T; i++) {
String line = in.nextLine();
char[] chars = line.toCharArray();
for (int j = 0; j < chars.length - 1; j++) {
while (!isLegal(chars, j)) {
if (--chars[j + 1] < '0') {
chars[j--]--;
}
for (int k = j + 2; k < chars.length; k++) {
chars[k] = '9';
}
}
}
System.out.println(new String(chars));
}
}
```
# 6. 微软-Spring Outing
下面以 N = 3K = 4 来进行讨论。
初始时,令第 0 个地方成为待定地点,也就是呆在家里。
从第 4 个地点开始投票,每个人只需要比较第 4 个地方和第 0 个地方的优先级,里,如果超过半数的人选择了第 4 个地方,那么更新第 4 个地方成为待定地点。
从后往前不断重复以上步骤,不断更新待定地点,直到所有地方都已经投票。
上面的讨论中,先令第 0 个地点成为待定地点,是因为这样的话第 4 个地点就只需要和这个地点进行比较,而不用考虑其它情况。如果最开始先令第 1 个地点成为待定地点,那么在对第 2 个地点进行投票时,每个人不仅要考虑第 2 个地点与第 1 个地点的优先级,也要考虑与其后投票地点的优先级。
```java
int N = in.nextInt();
int K = in.nextInt();
int[][] votes = new int[N][K + 1];
for (int i = 0; i < N; i++) {
for (int j = 0; j < K + 1; j++) {
int place = in.nextInt();
votes[i][place] = j;
}
}
int ret = 0;
for (int place = K; place > 0; place--) {
int cnt = 0;
for (int i = 0; i < N; i++) {
if (votes[i][place] < votes[i][ret]) {
cnt++;
}
}
if (cnt > N / 2) {
ret = place;
}
}
System.out.println(ret == 0 ? "otaku" : ret);
```
# 7. 微软-S-expression
# 8. 华为-最高分是多少
```java
int N = in.nextInt();
int M = in.nextInt();
int[] scores = new int[N];
for (int i = 0; i < N; i++) {
scores[i] = in.nextInt();
}
for (int i = 0; i < M; i++) {
String str = in.next();
if (str.equals("U")) {
int id = in.nextInt() - 1;
int newScore = in.nextInt();
scores[id] = newScore;
} else {
int idBegin = in.nextInt() - 1;
int idEnd = in.nextInt() - 1;
int ret = 0;
if (idBegin > idEnd) {
int t = idBegin;
idBegin = idEnd;
idEnd = t;
}
for (int j = idBegin; j <= idEnd; j++) {
ret = Math.max(ret, scores[j]);
}
System.out.println(ret);
}
}
```
# 9. 华为-简单错误记录
```java
HashMap<String, Integer> map = new LinkedHashMap<>();
while (in.hasNextLine()) {
String s = in.nextLine();
String key = s.substring(s.lastIndexOf('\\') + 1);
map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1);
}
List<Map.Entry<String, Integer>> list = new LinkedList<>(map.entrySet());
Collections.sort(list, (o1, o2) -> o2.getValue() - o1.getValue());
for (int i = 0; i < 8 && i < list.size(); i++) {
String[] token = list.get(i).getKey().split(" ");
String filename = token[0];
String line = token[1];
if (filename.length() > 16) filename = filename.substring(filename.length() - 16);
System.out.println(filename + " " + line + " " + list.get(i).getValue());
}
```
# 10. 华为-扑克牌大小
```java
public class Main {
private Map<String, Integer> map = new HashMap<>();
public Main() {
map.put("3", 0);
map.put("4", 1);
map.put("5", 2);
map.put("6", 3);
map.put("7", 4);
map.put("8", 5);
map.put("9", 6);
map.put("10", 7);
map.put("J", 8);
map.put("Q", 9);
map.put("K", 10);
map.put("A", 11);
map.put("2", 12);
map.put("joker", 13);
map.put("JOKER ", 14);
}
private String play(String s1, String s2) {
String[] token1 = s1.split(" ");
String[] token2 = s2.split(" ");
CardType type1 = computeCardType(token1);
CardType type2 = computeCardType(token2);
if (type1 == CardType.DoubleJoker) return s1;
if (type2 == CardType.DoubleJoker) return s2;
if (type1 == CardType.Bomb && type2 != CardType.Bomb) return s1;
if (type2 == CardType.Bomb && type1 != CardType.Bomb) return s2;
if (type1 != type2 || token1.length != token2.length) return "ERROR";
for (int i = 0; i < token1.length; i++) {
int val1 = map.get(token1[i]);
int val2 = map.get(token2[i]);
if (val1 != val2) return val1 > val2 ? s1 : s2;
}
return "ERROR";
}
private CardType computeCardType(String[] token) {
boolean hasjoker = false, hasJOKER = false;
for (int i = 0; i < token.length; i++) {
if (token[i].equals("joker")) hasjoker = true;
else if (token[i].equals("JOKER")) hasJOKER = true;
}
if (hasjoker && hasJOKER) return CardType.DoubleJoker;
int maxContinueLen = 1;
int curContinueLen = 1;
String curValue = token[0];
for (int i = 1; i < token.length; i++) {
if (token[i].equals(curValue)) curContinueLen++;
else {
curContinueLen = 1;
curValue = token[i];
}
maxContinueLen = Math.max(maxContinueLen, curContinueLen);
}
if (maxContinueLen == 4) return CardType.Bomb;
if (maxContinueLen == 3) return CardType.Triple;
if (maxContinueLen == 2) return CardType.Double;
boolean isStraight = true;
for (int i = 1; i < token.length; i++) {
if (map.get(token[i]) - map.get(token[i - 1]) != 1) {
isStraight = false;
break;
}
}
if (isStraight && token.length == 5) return CardType.Straight;
return CardType.Sigal;
}
private enum CardType {
DoubleJoker, Bomb, Sigal, Double, Triple, Straight;
}
public static void main(String[] args) {
Main main = new Main();
Scanner in = new Scanner(System.in);
while (in.hasNextLine()) {
String s = in.nextLine();
String[] token = s.split("-");
System.out.println(main.play(token[0], token[1]));
}
}
}
```
# 11. 去哪儿-二分查找
对于有重复元素的有序数组,二分查找需要注意以下要点:
- if (val <= A[m]) h = m;
- 因为 h 的赋值为 m 而不是 m - 1因此 while 循环的条件也就为 l < h如果是 m - 1 循环条件为 l <= h
```java
public int getPos(int[] A, int n, int val) {
int l = 0, h = n - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (val <= A[m]) h = m;
else l = m + 1;
}
return A[h] == val ? h : -1;
}
```
# 12. 去哪儿-首个重复字符
```java
public char findFirstRepeat(String A, int n) {
boolean[] hasAppear = new boolean[256];
for (int i = 0; i < n; i++) {
char c = A.charAt(i);
if(hasAppear[c]) return c;
hasAppear[c] = true;
}
return ' ';
}
```
# 13. 去哪儿-寻找Coder
```java
public String[] findCoder(String[] A, int n) {
List<Pair<String, Integer>> list = new ArrayList<>();
for (String s : A) {
int cnt = 0;
String t = s.toLowerCase();
int idx = -1;
while (true) {
idx = t.indexOf("coder", idx + 1);
if (idx == -1) break;
cnt++;
}
if (cnt != 0) {
list.add(new Pair<>(s, cnt));
}
}
Collections.sort(list, (o1, o2) -> (o2.getValue() - o1.getValue()));
String[] ret = new String[list.size()];
for (int i = 0; i < list.size(); i++) {
ret[i] = list.get(i).getKey();
}
return ret;
}
// 牛客网无法导入 javafx.util.Pair这里就自己实现一下 Pair 类
private class Pair<T, K> {
T t;
K k;
Pair(T t, K k) {
this.t = t;
this.k = k;
}
T getKey() {
return t;
}
K getValue() {
return k;
}
}
```
# 14. 美团-最大差值
贪心策略。
```java
public int getDis(int[] A, int n) {
int max = 0;
int soFarMin = A[0];
for (int i = 1; i < n; i++) {
if(soFarMin > A[i]) soFarMin = A[i];
else max = Math.max(max, A[i]- soFarMin);
}
return max;
}
```
# 15. 美团-棋子翻转
```java
public int[][] flipChess(int[][] A, int[][] f) {
int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for (int[] ff : f) {
for (int[] dd : direction) {
int r = ff[0] + dd[0] - 1, c = ff[1] + dd[1] - 1;
if(r < 0 || r > 3 || c < 0 || c > 3) continue;
A[r][c] ^= 1;
}
}
return A;
}
```
# 16. 美团-拜访
```java
private Set<String> paths;
private List<Integer> curPath;
public int countPath(int[][] map, int n, int m) {
paths = new HashSet<>();
curPath = new ArrayList<>();
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (map[i][j] == 1) {
map[i][j] = -1;
int[][] leftRightDirection = {{1, 0}, {-1, 0}};
int[][] topDownDirection = {{0, 1}, {0, -1}};
for (int[] lr : leftRightDirection) {
for (int[] td : topDownDirection) {
int[][] directions = {lr, td};
backtracking(map, n, m, i, j, directions);
}
}
return paths.size();
}
}
}
return 0;
}
private void backtracking(int[][] map, int n, int m, int r, int c, int[][] directions) {
if (map[r][c] == 2) {
String path = "";
for (int num : curPath) {
path += num;
}
paths.add(path);
return;
}
for (int i = 0; i < directions.length; i++) {
int nextR = r + directions[i][0];
int nextC = c + directions[i][1];
if (nextR < 0 || nextR >= n || nextC < 0 || nextC >= m || map[nextR][nextC] == -1) continue;
map[nextR][nextC] = map[nextR][nextC] == 2 ? 2 : -1;
curPath.add(nextR);
curPath.add(nextC);
backtracking(map, n, m, nextR, nextC, directions);
curPath.remove(curPath.size() - 1);
curPath.remove(curPath.size() - 1);
map[nextR][nextC] = map[nextR][nextC] == 2 ? 2 : 0;
}
}
```
# 17. 美团-直方图内最大矩形
```java
public int countArea(int[] A, int n) {
int max = 0;
for (int i = 0; i < n; i++) {
int min = A[i];
for (int j = i; j < n; j++) {
min = Math.min(min, A[j]);
max = Math.max(max, min * (j - i + 1));
}
}
return max;
}
```
# 18. 美团-字符串计数
字符串都是小写字符,可以把字符串当成是 26 进制。但是字典序的比较和普通的整数比较不同,是从左往右进行比较,例如 "ac" 和 "abc",字典序的比较结果为 "ac" > "abc",如果按照整数方法比较,因为 "abc" 是三位数,显然更大。
由于两个字符串的长度可能不想等,在 s1 空白部分和 s2 对应部分进行比较时,应该把 s1 的空白部分看成是 'a' 字符进行填充的。
还有一点要注意的是s1 到 s2 长度为 len<sub>i</sub> 的字符串个数只比较前面 i 个字符。例如 'aaa' 和 'bbb' ,长度为 2 的个数为 'aa' 到 'bb' 的字符串个数,不需要考虑后面部分的字符。
在统计个数时,从 len1 开始一直遍历到最大合法长度,每次循环都统计长度为 i 的子字符串个数。
```java
String s1 = in.next();
String s2 = in.next();
int len1 = in.nextInt();
int len2 = in.nextInt();
int len = Math.min(s2.length(), len2);
int[] subtractArr = new int[len];
for (int i = 0; i < len; i++) {
char c1 = i < s1.length() ? s1.charAt(i) : 'a';
char c2 = s2.charAt(i);
subtractArr[i] = c2 - c1;
}
int ret = 0;
for (int i = len1; i <= len; i++) {
for (int j = 0; j < i; j++) {
ret += subtractArr[j] * Math.pow(26, i - j - 1);
}
}
System.out.println(ret - 1);
```
# 19. 美团-平均年龄
```java
int W = in.nextInt();
double Y = in.nextDouble();
double x = in.nextDouble();
int N = in.nextInt();
while (N-- > 0) {
Y++; // 老员工每年年龄都要加 1
Y += (21 - Y) * x;
}
System.out.println((int) Math.ceil(Y));
```
# 20. 百度-罪犯转移
部分和问题,将每次求的部分和缓存起来。
```java
int n = in.nextInt();
int t = in.nextInt();
int c = in.nextInt();
int[] values = new int[n];
for (int i = 0; i < n; i++) {
values[i] = in.nextInt();
}
int cnt = 0;
int totalValue = 0;
for (int s = 0, e = c - 1; e < n; s++, e++) {
if (s == 0) {
for (int j = 0; j < c; j++) totalValue += values[j];
} else {
totalValue = totalValue - values[s - 1] + values[e];
}
if (totalValue <= t) cnt++;
}
System.out.println(cnt);
```
# 22. 百度-裁减网格纸
```java
int n = in.nextInt();
int minX, minY, maxX, maxY;
minX = minY = Integer.MAX_VALUE;
maxX = maxY = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int x = in.nextInt();
int y = in.nextInt();
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
System.out.println((int) Math.pow(Math.max(maxX - minX, maxY - minY), 2));
```
# 23. 百度-钓鱼比赛
P ( 至少钓一条鱼 ) = 1 - P ( 一条也钓不到 )
坑:读取概率矩阵的时候,需要一行一行进行读取,而不能直接用 in.nextDouble()。
```java
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
int n = in.nextInt();
int m = in.nextInt();
int x = in.nextInt();
int y = in.nextInt();
int t = in.nextInt();
in.nextLine(); // 坑
double pcc = 0.0;
double sum = 0.0;
for (int i = 1; i <= n; i++) {
String[] token = in.nextLine().split(" "); // 坑
for (int j = 1; j <= m; j++) {
double p = Double.parseDouble(token[j - 1]);
// double p = in.nextDouble();
sum += p;
if (i == x && j == y) {
pcc = p;
}
}
}
double pss = sum / (n * m);
pcc = computePOfIRT(pcc, t);
pss = computePOfIRT(pss, t);
System.out.println(pcc > pss ? "cc" : pss > pcc ? "ss" : "equal");
System.out.printf("%.2f\n", Math.max(pcc, pss));
}
}
// compute probability of independent repeated trials
private static double computePOfIRT(double p, int t) {
return 1 - Math.pow((1 - p), t);
}
```
# 24. 百度-蘑菇阵
这题用回溯会超时,需要用 DP。
dp[i][j] 表示到达 (i,j) 位置不会触碰蘑菇的概率。对于 N\*M 矩阵,如果 i == N || j == M那么 (i,j) 只能有一个移动方向;其它情况下能有两个移动方向。
考虑以下矩阵,其中第 3 行和第 3 列只能往一个方向移动,而其它位置可以有两个方向移动。
```java
int N = in.nextInt();
int M = in.nextInt();
int K = in.nextInt();
boolean[][] mushroom = new boolean[N][M];
while (K-- > 0) {
int x = in.nextInt();
int y = in.nextInt();
mushroom[x - 1][y - 1] = true;
}
double[][] dp = new double[N][M];
dp[0][0] = 1;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (mushroom[i][j]) dp[i][j] = 0;
else {
double cur = dp[i][j];
if (i == N - 1 && j == M - 1) break;
if (i == N - 1) dp[i][j + 1] += cur;
else if (j == M - 1) dp[i + 1][j] += cur;
else {
dp[i][j + 1] += cur / 2;
dp[i + 1][j] += cur / 2;
}
}
}
}
System.out.printf("%.2f\n", dp[N - 1][M - 1]);
```

158
notes/Git.md Normal file
View File

@ -0,0 +1,158 @@
<!-- GFM-TOC -->
* [学习资料](#学习资料)
* [集中式与分布式](#集中式与分布式)
* [Git 的中心服务器](#git-的中心服务器)
* [Git 工作流](#git-工作流)
* [分支实现](#分支实现)
* [冲突](#冲突)
* [Fast forward](#fast-forward)
* [分支管理策略](#分支管理策略)
* [储藏Stashing](#储藏stashing)
* [SSH 传输设置](#ssh-传输设置)
* [.gitignore 文件](#gitignore-文件)
* [Git 命令一览](#git-命令一览)
<!-- GFM-TOC -->
# 学习资料
- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
- [Learn Git Branching](https://learngitbranching.js.org/)
# 集中式与分布式
Git 属于分布式版本控制系统,而 SVN 属于集中式。
集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。
集中式版本控制有安全性问题,当中心服务器挂了所有人都没办法工作了。
集中式版本控制需要连网才能工作,如果网速过慢,那么提交一个文件的会慢的无法让人忍受。而分布式版本控制不需要连网就能工作。
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
# Git 的中心服务器
Git 的中心服务器用来交换每个用户的修改。没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。
# Git 工作流
<div align="center"> <img src="../pics//a1198642-9159-4d88-8aec-c3b04e7a2563.jpg"/> </div><br>
新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git它属于 Git 的版本库。
Git 版本库有一个称为 stage 的暂存区,还有自动创建的 master 分支以及指向分支的 HEAD 指针。
<div align="center"> <img src="../pics//46f66e88-e65a-4ad0-a060-3c63fe22947c.png"/> </div><br>
- git add files 把文件的修改添加到暂存区
- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
- git reset -- files 使用当前分支上的修改覆盖暂缓区,用来撤销最后一次 git add files
- git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改
<div align="center"> <img src="../pics//17976404-95f5-480e-9cb4-250e6aa1d55f.png"/> </div><br>
可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中
- git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交
- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
# 分支实现
Git 把每次提交都连成一条时间线。分支使用指针来实现,例如 master 分支指针指向时间线的最后一个节点也就是最后一次提交。HEAD 指针指向的是当前分支。
<div align="center"> <img src="../pics//fb546e12-e1fb-4b72-a1fb-8a7f5000dce6.jpg"/> </div><br>
新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
<div align="center"> <img src="../pics//bc775758-89ab-4805-9f9c-78b8739cf780.jpg"/> </div><br>
每次提交只会让当前分支向前移动,而其它分支不会移动。
<div align="center"> <img src="../pics//5292faa6-0141-4638-bf0f-bb95b081dcba.jpg"/> </div><br>
合并分支也只需要改变指针即可。
<div align="center"> <img src="../pics//1164a71f-413d-494a-9cc8-679fb6a2613d.jpg"/> </div><br>
# 冲突
当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。
<div align="center"> <img src="../pics//58e57a21-6b6b-40b6-af85-956dd4e0f55a.jpg"/> </div><br>
Git 会使用 <<<<<<< ======= >>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
```
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
```
# Fast forward
"快进式合并"fast-farward merge会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
```
$ git merge --no-ff -m "merge with no-ff" dev
```
<div align="center"> <img src="../pics//dd78a1fe-1ff3-4bcf-a56f-8c003995beb6.jpg"/> </div><br>
# 分支管理策略
master 分支应该是非常稳定的,只用来发布新版本;
日常开发在开发分支 dev 上进行。
<div align="center"> <img src="../pics//245fd2fb-209c-4ad5-bc5e-eb5664966a0e.jpg"/> </div><br>
# 储藏Stashing
在一个分支上操作之后,如果还没有将修改提交到分支上,此时进行切换分支,那么另一个分支上也能看到新的修改。这是因为所有分支都共用一个工作区的缘故。
可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈上,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
```
$ git stash
Saved working directory and index state \ "WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
```
该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
# SSH 传输设置
Git 仓库和 Github 中心仓库之间是通过 SSH 加密。
如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key
```
$ ssh-keygen -t rsa -C "youremail@example.com"
```
然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。
# .gitignore 文件
忽略以下文件:
1. 操作系统自动生成的文件,比如缩略图;
2. 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
3. 自己的敏感信息,比如存放口令的配置文件。
不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
# Git 命令一览
<div align="center"> <img src="../pics//7a29acce-f243-4914-9f00-f2988c528412.jpg"/> </div><br>
比较详细的地址http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf

717
notes/HTTP.md Normal file
View File

@ -0,0 +1,717 @@
<!-- GFM-TOC -->
* [一 、基础概念](#一-基础概念)
* [Web 基础](#web-基础)
* [URL](#url)
* [请求和响应报文](#请求和响应报文)
* [二、HTTP 方法](#二http-方法)
* [GET](#get)
* [POST](#post)
* [HEAD](#head)
* [PUT](#put)
* [PATCH](#patch)
* [DELETE](#delete)
* [OPTIONS](#options)
* [CONNECT](#connect)
* [TRACE](#trace)
* [三、HTTP 状态码](#三http-状态码)
* [2XX 成功](#2xx-成功)
* [3XX 重定向](#3xx-重定向)
* [4XX 客户端错误](#4xx-客户端错误)
* [5XX 服务器错误](#5xx-服务器错误)
* [四、HTTP 首部](#四http-首部)
* [通用首部字段](#通用首部字段)
* [请求首部字段](#请求首部字段)
* [响应首部字段](#响应首部字段)
* [实体首部字段](#实体首部字段)
* [五、具体应用](#五具体应用)
* [Cookie](#cookie)
* [缓存](#缓存)
* [持久连接](#持久连接)
* [编码](#编码)
* [分块传输](#分块传输)
* [多部分对象集合](#多部分对象集合)
* [范围请求](#范围请求)
* [内容协商](#内容协商)
* [虚拟主机](#虚拟主机)
* [通信数据转发](#通信数据转发)
* [六、HTTPs](#六https)
* [加密](#加密)
* [认证](#认证)
* [完整性](#完整性)
* [七、Web 攻击技术](#七web-攻击技术)
* [攻击模式](#攻击模式)
* [跨站脚本攻击](#跨站脚本攻击)
* [SQL 注入攻击](#sql-注入攻击)
* [跨站点请求伪造](#跨站点请求伪造)
* [拒绝服务攻击](#拒绝服务攻击)
* [八、各版本比较](#八各版本比较)
* [HTTP/1.0 与 HTTP/1.1 的区别](#http10-与-http11-的区别)
* [HTTP/1.1 与 HTTP/2.0 的区别](#http11-与-http20-的区别)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一 、基础概念
## Web 基础
- HTTPHyperText Transfer Protocol超文本传输协议
- WWWWorld Wide Web的三种技术HTML、HTTP、URL。
- RFCRequest for Comments征求修正意见书互联网的设计文档。
## URL
- URIUniform Resource Indentifier统一资源标识符
- URLUniform Resource Locator统一资源定位符
- URNUniform Resource Name统一资源名称例如 urn:isbn:0-486-27557-4 。
URI 包含 URL 和 URN目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
<div align="center"> <img src="../pics//4102b7d0-39b9-48d8-82ae-ac4addb7ebfb.jpg"/> </div><br>
## 请求和响应报文
### 1. 请求报文
<div align="center"> <img src="../pics//22b39f77-ac47-4978-91ed-84aaf457644c.jpg"/> </div><br>
### 2. 响应报文
<div align="center"> <img src="../pics//00d8d345-cd4a-48af-919e-209d2788eca7.jpg"/> </div><br>
# 二、HTTP 方法
客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。
## GET
> 获取资源
当前网络请求中,绝大部分使用的是 GET 方法。
## POST
> 传输实体主体
POST 主要目的不是获取资源,而是传输存储在内容实体中的数据。
GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在内容实体。
```
GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
```
```
POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2
```
GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。
GET 和 POST 的另一个区别是,使用 GET 方法,浏览器会把 HTTP Header 和 Data 一并发送出去,服务器响应 200OK并返回数据。而使用 POST 方法,浏览器先发送 Header服务器响应 100Continue之后浏览器再发送 Data最后服务器响应 200OK并返回数据。
## HEAD
> 获取报文首部
和 GET 方法一样,但是不返回报文实体主体部分。
主要用于确认 URL 的有效性以及资源更新的日期时间等。
## PUT
> 上传文件
由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。
```html
PUT /new.html HTTP/1.1
Host: example.com
Content-type: text/html
Content-length: 16
<p>New File</p>
```
## PATCH
> 对资源进行部分修改
PUT 也可以用于修改资源但是只能完全替代原始资源PATCH 允许部分修改。
```html
PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 100
[description of changes]
```
## DELETE
> 删除文件
与 PUT 功能相反,并且同样不带验证机制。
```html
DELETE /file.html HTTP/1.1
```
## OPTIONS
> 查询支持的方法
查询指定的 URL 能够支持的方法。
会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。
## CONNECT
> 要求用隧道协议连接代理
要求在于代理服务器通信时建立隧道,使用 SSLSecure Sokets Layer安全套接字和 TLSTransport Layer Security传输层安全协议把通信内容加密后经网络隧道传输。
```html
CONNECT www.example.com:443 HTTP/1.1
```
<div align="center"> <img src="../pics//5994928c-3d2d-45bd-abb1-adc4f5f4d775.jpg"/> </div><br>
## TRACE
> 追踪路径
服务器会将通信路径返回给客户端。
发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1当数值为 0 时就停止传输。
通常不会使用 TRACE并且它容易受到 XST 攻击Cross-Site Tracing跨站追踪因此更不会去使用它。
<div align="center"> <img src="../pics//c8637fd2-3aaa-46c4-b7d9-f24d3fa04781.jpg"/> </div><br>
# 三、HTTP 状态码
服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
| 状态码 | 类别 | 原因短语 |
| :---: | :---: | :---: |
| 1XX | Informational信息性状态码 | 接收的请求正在处理 |
| 2XX | Success成功状态码 | 请求正常处理完毕 |
| 3XX | Redirection重定向状态码 | 需要进行附加操作以完成请求 |
| 4XX | Client Error客户端错误状态码 | 服务器无法处理请求 |
| 5XX | Server Error服务器错误状态码 | 服务器处理请求出错 |
## 2XX 成功
- **200 OK**
- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
- **206 Partial Content** :表示客户端进行了范围请求。响应报文包含由 Content-Range 指定范围的实体内容。
## 3XX 重定向
- **301 Moved Permanently** :永久性重定向
- **302 Found** :临时性重定向
- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会 在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
- **304 Not Modified** 如果请求报文首部包含一些条件例如If-MatchIf-ModifiedSinceIf-None-MatchIf-RangeIf-Unmodified-Since但是不满足条件则服务器会返回 304 状态码。
- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
## 4XX 客户端错误
- **400 Bad Request** :请求报文中存在语法错误。
- **401 Unauthorized** 该状态码表示发送的请求需要有认证信息BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
<div align="center"> <img src="../pics//b1b4cf7d-c54a-4ff1-9741-cd2eea331123.jpg"/> </div><br>
- **403 Forbidden** :请求被拒绝,服务器端没有必要给出拒绝的详细理由。
- **404 Not Found**
## 5XX 服务器错误
- **500 Internal Server Error** :服务器正在执行请求时发生错误。
- **503 Service Unavilable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
# 四、HTTP 首部
有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。
各种首部字段及其含义如下(不需要全记,仅供查阅):
## 通用首部字段
| 首部字段名 | 说明 |
| :--: | :--: |
| Cache-Control | 控制缓存的行为 |
| Connection | 控制不再转发给代理的首部字段、管理持久连接|
| Date | 创建报文的日期时间 |
| Pragma | 报文指令 |
| Trailer | 报文末端的首部一览 |
| Transfer-Encoding | 指定报文主体的传输编码方式 |
| Upgrade | 升级为其他协议 |
| Via | 代理服务器的相关信息 |
| Warning | 错误通知 |
## 请求首部字段
| 首部字段名 | 说明 |
| :--: | :--: |
| Accept | 用户代理可处理的媒体类型 |
| Accept-Charset | 优先的字符集 |
| Accept-Encoding | 优先的内容编码 |
| Accept-Language | 优先的语言(自然语言) |
| Authorization | Web 认证信息 |
| Expect | 期待服务器的特定行为 |
| From | 用户的电子邮箱地址 |
| Host | 请求资源所在服务器 |
| If-Match | 比较实体标记ETag |
| If-Modified-Since | 比较资源的更新时间 |
| If-None-Match | 比较实体标记(与 If-Match 相反) |
| If-Range | 资源未更新时发送实体 Byte 的范围请求 |
| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) |
| Max-Forwards | 最大传输逐跳数 |
| Proxy-Authorization | 代理服务器要求客户端的认证信息 |
| Range | 实体的字节范围请求 |
| Referer | 对请求中 URI 的原始获取方 |
| TE | 传输编码的优先级 |
| User-Agent | HTTP 客户端程序的信息 |
## 响应首部字段
| 首部字段名 | 说明 |
| :--: | :--: |
| Accept-Ranges | 是否接受字节范围请求 |
| Age | 推算资源创建经过时间 |
| ETag | 资源的匹配信息 |
| Location | 令客户端重定向至指定 URI |
| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
| Retry-After | 对再次发起请求的时机要求 |
| Server | HTTP 服务器的安装信息 |
| Vary | 代理服务器缓存的管理信息 |
| WWW-Authenticate | 服务器对客户端的认证信息 |
## 实体首部字段
| 首部字段名 | 说明 |
| :--: | :--: |
| Allow | 资源可支持的 HTTP 方法 |
| Content-Encoding | 实体主体适用的编码方式 |
| Content-Language | 实体主体的自然语言 |
| Content-Length | 实体主体的大小 |
| Content-Location | 替代对应资源的 URI |
| Content-MD5 | 实体主体的报文摘要 |
| Content-Range | 实体主体的位置范围 |
| Content-Type | 实体主体的媒体类型 |
| Expires | 实体主体过期的日期时间 |
| Last-Modified | 资源的最后修改日期时间 |
# 五、具体应用
## Cookie
HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
Cookie 是服务器发送给客户端的数据,该数据会被保存在浏览器中,并且在下一次发送请求时包含该数据。通过 Cookie 可以让服务器知道两个请求是否来自于同一个客户端,从而实现保持登录状态等功能。
### 1. 创建过程
服务器发送的响应报文包含 Set-Cookie 字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
```html
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[page content]
```
客户端之后发送请求时,会从浏览器中读出 Cookie 值,在请求报文中包含 Cookie 字段。
```html
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
```
### 2. Set-Cookie
| 属性 | 说明 |
| :--: | -- |
| NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) |
| expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) |
| path=PATH | 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的文件目录) |
| domain=域名 | 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名) |
| Secure | 仅在 HTTPS 安全通信时才会发送 Cookie |
| HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 |
### 3. Session 和 Cookie 区别
Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文就包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是通过 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的Session 用于服务器端Cookie 用于客户端。
### 4. 浏览器禁用 Cookie 的情况
会使用 URL 重写技术,在 URL 后面加上 sid=xxx 。
### 5. 使用 Cookie 实现用户名和密码的自动填写
网站脚本会自动从保存在浏览器中的 Cookie 读取用户名和密码,从而实现自动填写。
## 缓存
### 1. 优点
1. 降低服务器的负担;
2. 提高响应速度(缓存资源比服务器上的资源离客户端更近)。
### 2. 实现方法
1. 让代理服务器进行缓存;
2. 让客户端浏览器进行缓存。
### 3. Cache-Control 字段
HTTP 通过 Cache-Control 首部字段来控制缓存。
```html
Cache-Control: private, max-age=0, no-cache
```
### 4. no-cache 指令
该指令出现在请求报文的 Cache-Control 字段中,表示缓存服务器需要先向原服务器验证缓存资源是否过期;
该指令出现在响应报文的 Cache-Control 字段中,表示缓存服务器在进行缓存之前需要先验证缓存资源的有效性。
### 5. no-store 指令
该指令表示缓存服务器不能对请求或响应的任何一部分进行缓存。
no-cache 不表示不缓存而是缓存之前需要先进行验证no-store 才是不进行缓存。
### 6. max-age 指令
该指令出现在请求报文的 Cache-Control 字段中,如果缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
该指令出现在响应报文的 Cache-Control 字段中,表示缓存资源在缓存服务器中保存的时间。
Expires 字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中Cache-Control : max-age 指令会被忽略掉。
## 持久连接
当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。持久连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
<div align="center"> <img src="../pics//c73a0b78-5f46-4d2d-a009-dab2a999b5d8.jpg"/> </div><br>
持久连接需要使用 Connection 首部字段进行管理。HTTP/1.1 开始 HTTP 默认是持久化连接的,如果要断开 TCP 连接,需要由客户端或者服务器端提出断开,使用 Connection : close而在 HTTP/1.1 之前默认是非持久化连接的,如果要维持持续连接,需要使用 Connection : Keep-Alive。
**管线化方式** 可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。
<div align="center"> <img src="../pics//6943e2af-5a70-4004-8bee-b33d60f39da3.jpg"/> </div><br>
## 编码
编码Encoding主要是为了对实体进行压缩。常用的编码有gzip、compress、deflate、identity其中 identity 表示不执行压缩的编码格式。
## 分块传输
分块传输Chunked Transfer Coding可以把数据分割成多块让浏览器逐步显示页面。
## 多部分对象集合
一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。
例如,上传多个表单时可以使用如下方式:
```html
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
Content-Disposition: form-data; name="submit-name"
Larry
--AaB03x
Content-Disposition: form-data; name="files"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
```
## 范围请求
如果网络出现中断,服务器只发送了一部分数据,范围请求使得客户端能够只请求未发送的那部分数据,从而避免服务器端重新发送所有数据。
在请求报文首部中添加 Range 字段,然后指定请求的范围,例如 Range:bytes=5001-10000。请求成功的话服务器发送 206 Partial Content 状态。
```html
GET /z4d4kWk.jpg HTTP/1.1
Host: i.imgur.com
Range: bytes=0-1023
```
```html
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
...
(binary content)
```
## 内容协商
通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。
涉及以下首部字段Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。
## 虚拟主机
使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
## 通信数据转发
### 1. 代理
代理服务器接受客户端的请求,并且转发给其它服务器。
代理服务器一般是透明的,不会改变 URL。
使用代理的主要目的是:缓存、网络访问控制以及访问日志记录。
<div align="center"> <img src="../pics//c07035c3-a9ba-4508-8e3c-d8ae4c6ee9ee.jpg"/> </div><br>
### 2. 网关
与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。
<div align="center"> <img src="../pics//81375888-6be1-476f-9521-42eea3e3154f.jpg"/> </div><br>
### 3. 隧道
使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。
<div align="center"> <img src="../pics//64b95403-d976-421a-8b45-bac89c0b5185.jpg"/> </div><br>
# 六、HTTPs
HTTP 有以下安全性问题:
1. 使用明文进行通信,内容可能会被窃听;
2. 不验证通信方的身份,通信方的身份有可能遭遇伪装;
3. 无法证明报文的完整性,报文有可能遭篡改。
HTTPs 并不是新协议,而是 HTTP 先和 SSLSecure Socket Layer通信再由 SSL 和 TCP 通信。通过使用 SSLHTTPs 提供了加密、认证和完整性保护。
## 加密
有两种加密方式:对称密钥加密和公开密钥加密。对称密钥加密的加密和解密使用同一密钥,而公开密钥加密使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
对称密钥加密的缺点:无法安全传输密钥;公开密钥加密的缺点:相对来说更耗时。
HTTPs 采用 **混合的加密机制** ,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。(下图中,共享密钥即对称密钥)
<div align="center"> <img src="../pics//110b1a9b-87cd-45c3-a21d-824623715b33.jpg"/> </div><br>
## 认证
通过使用 **证书** 来对通信方进行认证。
数字证书认证机构CACertificate Authority是客户端与服务器双方都可信赖的第三方机构。服务器的运营人员向 CA 提出公开密钥的申请CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
进行 HTTPs 通信时,服务器会把证书发送给客户端,客户端取得其中的公开密钥之后,先进行验证,如果验证通过,就可以开始通信。
除了上诉提到的服务器端证书之外,还有客户端证书,客户端证书的目的就是让服务器对客户端进行验证。客户端证书需要用户自行安装,只有在业务需要非常高的安全性时才使用客户端证书,例如网上银行。
使用 OpenSSL 这套开源程序,每个人都可以构建一套属于自己的认证机构,从而自己给自己颁发服务器证书。浏览器在访问该服务器时,会显示“无法确认连接安全性”或“该网站的安全证书存在问题”等警告消息。
## 完整性
SSL 提供摘要功能来验证完整性。
# 七、Web 攻击技术
Web 攻击的主要目标是使用 HTTP 协议的 Web 应用。
## 攻击模式
### 1. 主动攻击
直接攻击服务器,具有代表性的有 SQL 注入和 OS 命令注入。
### 2. 被动攻击
设下圈套,让用户发送有攻击代码的 HTTP 请求,那么用户发送了该 HTTP 请求之后就会泄露 Cookie 等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。
## 跨站脚本攻击
### 1. 概念
Cross-Site Scripting, XSS可以将代码注入到用户浏览的网页上这种代码包含 HTML 和 JavaScript。通过利用网页开发时留下的漏洞通过巧妙的方法注入恶意指令代码到网页使用户加载并执行攻击者恶意制造的网页程序。攻击成功后攻击者可能得到更高的权限如执行一些操作、私密网页内容、会话和 Cookie 等各种内容。
### 2. 危害
- 伪造虚假的输入表单骗取个人信息
- 窃取用户的 Cookie 值
- 显示伪造的文章或者图片
### 3. 防范手段
**(一)过滤特殊字符**
许多语言都提供了对 HTML 的过滤:
- PHP 的 htmlentities() 或是 htmlspecialchars()。
- Python 的 cgi.escape()。
- Java 的 xssprotect (Open Source Library)。
- Node.js 的 node-validator。
**(二)指定 HTTP 的 Content-Type**
通过这种方式,可以避免内容被当成 HTML 解析,比如 PHP 语言可以使用以下代码:
```php
<?php
header('Content-Type: text/javascript; charset=utf-8');
?>
```
## SQL 注入攻击
### 1. 概念
服务器上的数据库运行非法的 SQL 语句。
### 2. 攻击原理
例如一个网站登录验证的 SQL 查询代码为:
```sql
strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
```
如果填入以下内容:
```sql
userName = "1' OR '1'='1";
passWord = "1' OR '1'='1";
```
那么 SQL 查询字符串为:
```sql
strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"
```
此时无需验证通过就能执行以下查询:
```sql
strSQL = "SELECT * FROM users;"
```
### 3. 危害
- 数据表中的数据外泄,例如个人机密数据,账户数据,密码等。
- 数据结构被黑客探知,得以做进一步攻击(例如 SELECT * FROM sys.tables
- 数据库服务器被攻击,系统管理员账户被窜改(例如 ALTER LOGIN sa WITH PASSWORD='xxxxxx')。
- 获取系统较高权限后,有可能得以在网页加入恶意链接、恶意代码以及 XSS 等。
- 经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统(例如 xp_cmdshell "net stop iisadmin" 可停止服务器的 IIS 服务)。
- 破坏硬盘数据,瘫痪全系统(例如 xp_cmdshell "FORMAT C:")。
### 4. 防范手段
- 在设计应用程序时完全使用参数化查询Parameterized Query来设计数据访问功能。
- 在组合 SQL 字符串时,先针对所传入的参数作字符取代(将单引号字符取代为连续 2 个单引号字符)。
- 如果使用 PHP 开发网页程序的话,亦可打开 PHP 的魔术引号Magic quote功能自动将所有的网页传入参数将单引号字符取代为连续 2 个单引号字符)。
- 其他,使用其他更安全的方式连接 SQL 数据库。例如已修正过 SQL 注入问题的数据库连接组件,例如 ASP.NET 的 SqlDataSource 对象或是 LINQ to SQL。
- 使用 SQL 防注入系统。
## 跨站点请求伪造
### 1. 概念
Cross-site request forgeryXSRF是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作如发邮件发消息甚至财产操作如转账和购买商品。由于浏览器曾经认证过所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
XSS 利用的是用户对指定网站的信任CSRF 利用的是网站对用户网页浏览器的信任。
假如一家银行用以执行转账操作的 URL 地址如下:`http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName`。
<div align="center"> 那么,一个恶意攻击者可以在另一个网站上放置如下代码:`<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">`。 </div><br>
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
### 2. 防范手段
**(一)检查 Referer 字段**
HTTP 头中有一个 Referer 字段这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时通常来说Referer 字段应和请求的地址位于同一域名下。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
**(二)添加校验 Token**
由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 token 的值为空或者错误,拒绝这个可疑请求。
## 拒绝服务攻击
### 1. 概念
denial-of-service attackDoS亦称洪水攻击其目的在于使目标电脑的网络或系统资源耗尽使服务暂时中断或停止导致其正常用户无法访问。
distributed denial-of-service attackDDoS指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
> [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)
# 八、各版本比较
## HTTP/1.0 与 HTTP/1.1 的区别
HTTP/1.1 新增了以下内容:
- 默认为长连接;
- 提供了范围请求功能;
- 提供了虚拟主机的功能;
- 多了一些缓存处理字段;
- 多了一些状态码。
## HTTP/1.1 与 HTTP/2.0 的区别
### 1. 多路复用
HTTP/2.0 使用多路复用技术,使用同一个 TCP 连接来处理多个请求。
### 2. 首部压缩
HTTP/1.1 的首部带有大量信息而且每次都要重复发送。HTTP/2.0 要求通讯双方各自缓存一份首部字段表,从而避免了重复传输。
### 3. 服务端推送
在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。
### 4. 二进制格式
HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。
# 参考资料
- [图解 HTTP](https://pan.baidu.com/s/1M0AHXqG9sP9Bxne6u0JK8A)
- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
- [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC)
- [维基百科SQL 注入攻击](https://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A)
- [维基百科:跨站点请求伪造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
- [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)

View File

@ -0,0 +1,283 @@
<!-- GFM-TOC -->
* [一、创建型](#一创建型)
* [1. 单例模式](#1-单例模式)
* [2. 简单工厂模式](#2-简单工厂模式)
* [3. 工厂方法模式](#3-工厂方法模式)
* [4. 抽象工厂](#4-抽象工厂)
* [5. 生成器模式](#5-生成器模式)
* [6. 原型模式](#6-原型模式)
* [二、行为型](#二行为型)
* [1. 责任链](#1-责任链)
* [2. 命令模式](#2-命令模式)
* [3. 解释器模式](#3-解释器模式)
* [4. 迭代器](#4-迭代器)
* [5. 中间人模式](#5-中间人模式)
* [6. 备忘录模式](#6-备忘录模式)
* [7. 观察者模式](#7-观察者模式)
* [8. 策略模式](#8-策略模式)
* [9. 模板方法](#9-模板方法)
* [10. 访问者模式](#10-访问者模式)
* [11. 空对象模式](#11-空对象模式)
* [三、结构型](#三结构型)
* [1. 适配器](#1-适配器)
* [2. 桥接模式](#2-桥接模式)
* [3. 组合模式](#3-组合模式)
* [4. 装饰者模式](#4-装饰者模式)
* [5. 蝇量模式](#5-蝇量模式)
* [6. 动态代理](#6-动态代理)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、创建型
## 1. 单例模式
确保只实例化一个对象,并提供一个对象的全局访问点。
```java
java.lang.Runtime#getRuntime()
java.awt.Toolkit#getDefaultToolkit()
java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()
java.awt.Desktop#getDesktop()
```
## 2. 简单工厂模式
在不对用户暴露对象内部逻辑的前提下创建对象;使用通用的接口来创建对象;
## 3. 工厂方法模式
定义创建对象的接口,但是让子类来决定应该使用哪个类来创建;使用通用的接口来创建对象;
```java
java.lang.Proxy#newProxyInstance()
java.lang.Object#toString()
java.lang.Class#newInstance()
java.lang.reflect.Array#newInstance()
java.lang.reflect.Constructor#newInstance()
java.lang.Boolean#valueOf(String)
java.lang.Class#forName()
```
## 4. 抽象工厂
提供一个创建相关对象家族的接口,而没有明确指明它们的类。
```java
java.util.Calendar#getInstance()
java.util.Arrays#asList()
java.util.ResourceBundle#getBundle()
java.sql.DriverManager#getConnection()
java.sql.Connection#createStatement()
java.sql.Statement#executeQuery()
java.text.NumberFormat#getInstance()
javax.xml.transform.TransformerFactory#newInstance()
```
## 5. 生成器模式
定义一个新的类来构造另一个类的实例,以创建一个复杂的对象。
它可以封装一个对象的构造过程,并允许按步骤构造。
```java
java.lang.StringBuilder#append()
java.lang.StringBuffer#append()
java.sql.PreparedStatement
javax.swing.GroupLayout.Group#addComponent()
```
## 6. 原型模式
使用原型实例指定要创建对象的类型;通过复制这个原型来创建新对象。
```java
java.lang.Object#clone()
java.lang.Cloneable
```
# 二、行为型
## 1. 责任链
避免将请求的发送者附加到其接受者,从而使其它对象也可以处理请求;将请求以对象的方式把发送到链上直到请求被处理完毕。
```java
java.util.logging.Logger#log()
javax.servlet.Filter#doFilter()
```
## 2. 命令模式
将命令封装进对象中;允许使用命令对象对客户对象进行参数化;允许将命令对象存放到队列中。
```java
java.lang.Runnable
javax.swing.Action
```
## 3. 解释器模式
为语言创建解释器,通常由语言的语法和语法分析来定义。
```java
java.util.Pattern
java.text.Normalizer
java.text.Format
```
## 4. 迭代器
提供一种一致的访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。
```java
java.util.Iterator
java.util.Enumeration
```
## 5. 中间人模式
使用中间人对象来封装对象之间的交互。中间人模式可以让降低交互对象之间的耦合程度。
```java
java.util.Timer
java.util.concurrent.Executor#execute()
java.util.concurrent.ExecutorService#submit()
java.lang.reflect.Method#invoke()
```
## 6. 备忘录模式
在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到最初状态。
```java
java.util.Date
java.io.Serializable
```
## 7. 观察者模式
定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。
```java
java.util.EventListener
javax.servlet.http.HttpSessionBindingListener
javax.servlet.http.HttpSessionAttributeListener
javax.faces.event.PhaseListener
```
## 8. 策略模式
定义一系列算法,封装每个算法,并使它们可以互换。策略可以让算法独立于使用它的客户端。
```java
java.util.Comparator#compare()
javax.servlet.http.HttpServlet
javax.servlet.Filter#doFilter()
```
## 9. 模板方法
定义算法框架,并将一些步骤的实现延迟到子类。通过模板方法,子类可以重新定义算法的某些步骤,而不用改变算法的结构。
```java
java.util.Collections#sort()
java.io.InputStream#skip()
java.io.InputStream#read()
java.util.AbstractList#indexOf()
```
## 10. 访问者模式
提供便捷的维护方式来操作一组对象。它使你在不改变操作对象的前提下,可以修改或扩展对象的行为。
例如集合,它可以包含不同类型的元素,访问者模式允许在不知道具体元素类型的前提下对集合元素进行一些操作。
```java
javax.lang.model.element.Element and javax.lang.model.element.ElementVisitor
javax.lang.model.type.TypeMirror and javax.lang.model.type.TypeVisitor
```
## 11. 空对象模式
使用什么都不做的空对象来替代 NULL。
# 三、结构型
## 1. 适配器
把一个类接口转换成另一个用户需要的接口。
```java
java.util.Arrays#asList()
javax.swing.JTable(TableModel)
java.io.InputStreamReader(InputStream)
java.io.OutputStreamWriter(OutputStream)
javax.xml.bind.annotation.adapters.XmlAdapter#marshal()
javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal()
```
## 2. 桥接模式
将抽象与实现分离开来,使它们可以独立变化。
```java
AWT (It provides an abstraction layer which maps onto the native OS the windowing support.)
JDBC
```
## 3. 组合模式
将对象组合成树形结构来表示整理-部分层次关系,允许用户以相同的方式处理单独对象和组合对象。
```java
javax.swing.JComponent#add(Component)
java.awt.Container#add(Component)
java.util.Map#putAll(Map)
java.util.List#addAll(Collection)
java.util.Set#addAll(Collection)
```
## 4. 装饰者模式
为对象动态添加功能。
```java
java.io.BufferedInputStream(InputStream)
java.io.DataInputStream(InputStream)
java.io.BufferedOutputStream(OutputStream)
java.util.zip.ZipOutputStream(OutputStream)
java.util.Collections#checked[List|Map|Set|SortedSet|SortedMap]()
```
## 5. 蝇量模式
利用共享的方式来支持大量的对象,这些对象一部分内部状态时相同的,而另一份状态可以变化。
Java 利用缓存来加速大量小对象的访问时间。
```java
java.lang.Integer#valueOf(int)
java.lang.Boolean#valueOf(boolean)
java.lang.Byte#valueOf(byte)
java.lang.Character#valueOf(char)
```
## 6. 动态代理
提供一个占位符来控制对象的访问。
代理可以是一些轻量级的对象,它控制者对重量级对象的访问,只有在真正实例化这些重量级对象时才会去实例化它。
```java
java.lang.reflect.Proxy
RMI
```
# 参考资料
- [The breakdown of design patterns in JDK](http://www.programering.com/a/MTNxAzMwATY.html)
- [Design Patterns](http://www.oodesign.com/)

404
notes/Java IO.md Normal file
View File

@ -0,0 +1,404 @@
<!-- GFM-TOC -->
* [一、概览](#一概览)
* [二、磁盘操作](#二磁盘操作)
* [三、字节操作](#三字节操作)
* [四、字符操作](#四字符操作)
* [五、对象操作](#五对象操作)
* [六、网络操作](#六网络操作)
* [InetAddress](#inetaddress)
* [URL](#url)
* [Sockets](#sockets)
* [Datagram](#datagram)
* [七、NIO](#七nio)
* [流与块](#流与块)
* [通道与缓冲区](#通道与缓冲区)
* [缓冲区状态变量](#缓冲区状态变量)
* [文件 NIO 实例](#文件-nio-实例)
* [套接字 NIO 实例](#套接字-nio-实例)
* [内存映射文件](#内存映射文件)
* [对比](#对比)
* [八、参考资料](#八参考资料)
<!-- GFM-TOC -->
# 一、概览
Java 的 I/O 大概可以分成以下几类:
1. 磁盘操作File
2. 字节操作InputStream 和 OutputStream
3. 字符操作Reader 和 Writer
4. 对象操作Serializable
5. 网络操作Socket
6. 新的输入/输出NIO
# 二、磁盘操作
File 类可以用于表示文件和目录,但是它只用于表示文件的信息,而不表示文件的内容。
# 三、字节操作
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br>
Java I/O 使用了装饰者模式来实现。以 InputStream 为例InputStream 是抽象组件FileInputStream 是 InputStream 的子类属于具体组件提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
```java
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
```
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
批量读入文件内容到字节数组:
```java
byte[] buf = new byte[20*1024];
int bytes = 0;
// 最多读取 buf.length 个字节,返回的是实际读取的个数,返回 -1 的时候表示读到 eof即文件尾
while((bytes = in.read(buf, 0 , buf.length)) != -1) {
// ...
}
```
# 四、字符操作
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
InputStreamReader 实现从文本文件的字节流解码成字符流OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们继承自 Reader 和 Writer。
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
```java
byte[] bytes = str.getBytes(encoding); // 编码
String str = new String(bytes, encoding) // 解码
```
GBK 编码中,中文占 2 个字节,英文占 1 个字节UTF-8 编码中,中文占 3 个字节,英文占 1 个字节Java 使用双字节编码 UTF-16be中文和英文都占 2 个字节。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
# 五、对象操作
序列化就是将一个对象转换成字节序列,方便存储和传输。
序列化ObjectOutputStream.writeObject()
反序列化ObjectInputStream.readObject()
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现。
transient 关键字可以使一些属性不会被序列化。
**ArrayList 序列化和反序列化的实现** ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
```java
private transient Object[] elementData;
```
# 六、网络操作
Java 中的网络支持:
1. InetAddress用于表示网络上的硬件资源即 IP 地址;
2. URL统一资源定位符通过 URL 可以直接读取或者写入网络上的数据;
3. Sockets使用 TCP 协议实现网络通信;
4. Datagram使用 UDP 协议实现网络通信。
## InetAddress
没有公有构造函数,只能通过静态方法来创建实例。
```java
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] addr);
```
## URL
可以直接从 URL 中读取字节流数据
```java
URL url = new URL("http://www.baidu.com");
InputStream is = url.openStream(); // 字节流
InputStreamReader isr = new InputStreamReader(is, "utf-8"); // 字符流
BufferedReader br = new BufferedReader(isr);
String line = br.readLine();
while (line != null) {
System.out.println(line);
line = br.readLine();
}
br.close();
isr.close();
is.close();
```
## Sockets
- ServerSocket服务器端类
- Socket客户端类
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
<div align="center"> <img src="../pics//ClienteServidorSockets1521731145260.jpg"/> </div><br>
## Datagram
- DatagramPacket数据包类
- DatagramSocket通信类
# 七、NIO
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。
## 流与块
I/O 与 NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
一个面向块的 I/O 系统以块的形式处理数据,一次处理数据块。按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
## 通道与缓冲区
### 1. 通道
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
- FileChannel从文件中读写数据
- DatagramChannel通过 UDP 读写网络中数据;
- SocketChannel通过 TCP 读写网络中数据;
- ServerSocketChannel可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
### 2. 缓冲区
发送给一个通道的所有对象都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区包括以下类型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
## 缓冲区状态变量
- capacity最大容量
- position当前已经读写的字节数
- limit还可以读写的字节数。
状态变量的改变过程举例:
① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0而 limit = capacity = 9。capacity 变量不会改变,下面的讨论会忽略它。
<div align="center"> <img src="../pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
② 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3limit 保持不变。
<div align="center"> <img src="../pics//4628274c-25b6-4053-97cf-d1239b44c43d.png"/> </div><br>
③ 以下图例为已经从输入通道读取了 5 个字节数据写入缓冲区中。在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position并将 position 设置为 0。
<div align="center"> <img src="../pics//952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
<div align="center"> <img src="../pics//b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
<div align="center"> <img src="../pics//67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
## 文件 NIO 实例
① 为要读取的文件创建 FileInputStream之后通过 FileInputStream 获取输入 FileChannel
```java
FileInputStream fin = new FileInputStream("readandshow.txt");
FileChannel fic = fin.getChannel();
```
② 创建一个容量为 1024 的 Buffer
```java
ByteBuffer buffer = ByteBuffer.allocate(1024);
```
③ 将数据从输入 FileChannel 写入到 Buffer 中如果没有数据的话read() 方法会返回 -1
```java
int r = fcin.read(buffer);
if (r == -1) {
break;
}
```
④ 为要写入的文件创建 FileOutputStream之后通过 FileOutputStream 获取输出 FileChannel
```java
FileOutputStream fout = new FileOutputStream("writesomebytes.txt");
FileChannel foc = fout.getChannel();
```
⑤ 调用 flip() 切换读写
```java
buffer.flip();
```
⑥ 把 Buffer 中的数据读取到输出 FileChannel 中
```java
foc.write(buffer);
```
⑦ 最后调用 clear() 重置缓冲区
```java
buffer.clear();
```
## 套接字 NIO 实例
### 1. ServerSocketChannel
每一个监听端口都需要有一个 ServerSocketChannel 用来监听连接。
```java
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 设置为非阻塞
ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress(ports[i]);
ss.bind(address); // 绑定端口号
```
### 2. Selectors
异步 I/O 通过 Selector 注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接等等,在发生这样的事件时,系统将会发送通知。
创建 Selectors 之后,就可以对不同的通道对象调用 register() 方法。register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT这里它指定我们想要监听 ACCEPT 事件,也就是在新的连接建立时所发生的事件。
SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。
```java
Selector selector = Selector.open();
SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
```
### 3. 主循环
首先,我们调用 Selector 的 select() 方法。这个方法会阻塞直到至少有一个已注册的事件发生。当一个或者更多的事件发生时select() 方法将返回所发生的事件的数量。
接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个集合。
我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
```java
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey)it.next();
// ... deal with I/O event ...
}
```
### 4. 监听新连接
程序执行到这里,我们仅注册了 ServerSocketChannel并且仅注册它们“接收”事件。为确认这一点我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件:
```java
if ((key.readyOps() & SelectionKey.OP_ACCEPT)
== SelectionKey.OP_ACCEPT) {
// Accept the new connection
// ...
}
```
可以肯定地说readOps() 方法告诉我们该事件是新的连接。
### 5. 接受新的连接
因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞:
```java
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
```
下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector 上,如下所示:
```java
sc.configureBlocking(false);
SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ);
```
注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于读取而不是接受新连接。
### 6. 删除处理过的 SelectionKey
在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey
```java
it.remove();
```
现在我们可以返回主循环并接受从一个套接字中传入的数据 (或者一个传入的 I/O 事件) 了。
### 7. 传入的 I/O
当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示:
```java
} else if ((key.readyOps() & SelectionKey.OP_READ)
== SelectionKey.OP_READ) {
// Read the data
SocketChannel sc = (SocketChannel)key.channel();
// ...
}
```
## 内存映射文件
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
只有文件中实际读取或者写入的部分才会映射到内存中。
现代操作系统一般会根据需要将文件的部分映射为内存的部分从而实现文件系统。Java 内存映射机制只不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
向内存映射文件写入可能是危险的,仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
## 对比
NIO 与普通 I/O 的区别主要有以下两点:
- NIO 是非阻塞的。应当注意FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。
- NIO 面向流I/O 面向块。
# 八、参考资料
- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)

632
notes/Java 基础.md Normal file
View File

@ -0,0 +1,632 @@
<!-- GFM-TOC -->
* [一、关键字](#一关键字)
* [final](#final)
* [static](#static)
* [二、Object 通用方法](#二object-通用方法)
* [概览](#概览)
* [clone()](#clone)
* [equals()](#equals)
* [四、继承](#四继承)
* [访问权限](#访问权限)
* [抽象类与接口](#抽象类与接口)
* [super](#super)
* [重载与重写](#重载与重写)
* [五、String](#五string)
* [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder)
* [String 不可变的原因](#string-不可变的原因)
* [String.intern()](#stringintern)
* [六、基本类型与运算](#六基本类型与运算)
* [包装类型](#包装类型)
* [switch](#switch)
* [七、反射](#七反射)
* [八、异常](#八异常)
* [九、泛型](#九泛型)
* [十、注解](#十注解)
* [十一、特性](#十一特性)
* [面向对象三大特性](#面向对象三大特性)
* [Java 各版本的新特性](#java-各版本的新特性)
* [Java 与 C++ 的区别](#java-与-c++-的区别)
* [JRE or JDK](#jre-or-jdk)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、关键字
## final
**1. 数据**
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
- 对于基本类型final 使数值不变;
- 对于引用类型final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
```java
final int x = 1;
x = 2; // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;
```
**2. 方法** </font> </br>
声明方法不能被子类覆盖。
private 方法隐式地被指定为 final如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是重载了。
**3. 类**
声明类不允许被继承。
## static
**1. 静态变量**
静态变量在内存中只存在一份,只在类第一次实例化时初始化一次。
- 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它;
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
```java
public class A {
private int x; // 实例变量
public static int y; // 静态变量
}
```
**2. 静态方法**
静态方法在类加载的时候就存在了,它不依赖于任何实例,所以 static 方法必须实现也就是说它不能是抽象方法abstract
**3. 静态语句块**
静态语句块和静态变量一样在类第一次实例化时运行一次。
**4. 初始化顺序**
静态数据优先于其它数据的初始化,静态变量和静态语句块哪个先运行取决于它们在代码中的顺序。
```java
public static String staticField = "静态变量";
```
```java
static {
System.out.println("静态语句块");
}
```
实例变量和普通语句块的初始化在静态变量和静态语句块初始化结束之后。
```java
public String field = "实例变量";
```
```java
{
System.out.println("普通语句块");
}
```
最后才是构造函数中的数据进行初始化
```java
public InitialOrderTest() {
System.out.println("构造函数");
}
```
存在继承的情况下,初始化顺序为:
1. 父类(静态变量、静态语句块块)
2. 子类(静态变量、静态语句块)
3. 父类(实例变量、普通语句块)
4. 父类(构造函数)
5. 子类(实例变量、普通语句块)
6. 子类(构造函数)
# 二、Object 通用方法
## 概览
```java
public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
protected void finalize() throws Throwable {}
```
## clone()
**1. cloneable**
clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去重载 clone() 就没有这个方法。
```java
public class CloneTest {
private int a;
private int b;
}
```
```java
CloneTest x = new CloneTest();
CloneTest y = x.clone(); // 'clone()' has protected access in 'java.lang.Object'
```
接下来重载 Object 的 clone() 得到以下实现:
```java
public class CloneTest{
private int a;
private int b;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
```
```java
CloneTest x = new CloneTest();
try {
CloneTest y = (CloneTest) x.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
```
```html
java.lang.CloneNotSupportedException: CloneTest
```
以上抛出了 CloneNotSupportedException这是因为 CloneTest 没有实现 Cloneable 接口。应该注意的是clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protect 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
**2. 深拷贝与浅拷贝**
<div align="center"> <img src="../pics//CLone_20_281_29.png" width="800"/> </div><br>
- 浅拷贝:拷贝对象和原对象的引用类型引用同一个对象;
- 深拷贝:引用不同对象。
实现深拷贝的方法:
- [Defensive copying](http://www.javapractices.com/topic/TopicAction.do?Id=15)
- [copy constructors](http://www.javapractices.com/topic/TopicAction.do?Id=12)
- [static factory methods](http://www.javapractices.com/topic/TopicAction.do?Id=21).
> [How do I copy an object in Java?](https://stackoverflow.com/questions/869033/how-do-i-copy-an-object-in-java)
## equals()
**1. == 与 equals() 区别**
- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
- 对于引用类型,== 判断两个引用是否引用同一个对象,而 equals() 判断引用的对象是否等价。
**2. 等价性**
> [散列](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md#%E4%B8%89%E6%95%A3%E5%88%977)
# 四、继承
## 访问权限
Java 中有三个访问权限修饰符private、protected 以及 public如果不加访问修饰符表示包级可见。
可以对类或类中的成员(字段以及方法)加上访问修饰符。
- 成员可见表示其它类可以用成员所在类的对象访问到该成员;
- 类可见表示其它类可以用这个类创建对象。
在理解类的可见性时,可以把类当做包中的一个成员,然后包表示一个类,那么就可以类比成员的可见性。
protected 用于修饰成员,表示在继承体系中成员对于子类可见。但是这个访问修饰符对于类没有意义,因为包没有继承体系。
> [浅析 Java 中的访问权限控制](http://www.importnew.com/18097.html)
## 抽象类与接口
**1. 抽象类**
抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
```java
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
// abstract method
abstract void service(ServletRequest req, ServletResponse res);
void init() {
// Its implementation
}
// other method related to Servlet
}
```
> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
**2. 接口**
接口是抽象类的延伸。Java 为了安全性而不支持多重继承,一个类只能有一个父类。但是接口不同,一个类可以同时实现多个接口,不管这些接口之间有没有关系,所以接口弥补不支持多重继承的缺陷。
```java
public interface Externalizable extends Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
```
从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
```java
public interface InterfaceDefaultTest {
default void func() {
System.out.println("default method in interface!");
}
}
```
**3. 比较**
- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求子类和父类具有 IS-A 关系;
- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
**4. 使用选择**
使用抽象类:
- 需要在几个相关的类中共享代码;
- 需要能控制继承来的方法和字段的访问权限,而不是都为 public。
- 需要继承非静态non-static和非常量non-final字段。
使用接口:
- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
- 需要使用多重继承。
> [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
## super
**1. 访问父类的成员**
如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
```java
public class Superclass {
public void printMethod() {
System.out.println("Printed in Superclass.");
}
}
```
```java
public class Subclass extends Superclass {
// Overrides printMethod in Superclass
public void printMethod() {
super.printMethod();
System.out.println("Printed in Subclass");
}
public static void main(String[] args) {
Subclass s = new Subclass();
s.printMethod();
}
}
```
**2. 访问父类的构造函数**
可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。
```java
public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear) {
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
```
> [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
## 重载与重写
- 重写存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法;
- 重载即存在于继承体系中,也存在于同一个类中,指一个方法与已经存在的方法或者父类的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
# 五、String
## String, StringBuffer and StringBuilder
**1. 是否可变**
- String 不可变
- StringBuffer 和 StringBuilder 可变
**2. 是否线程安全**
- String 不可变,因此是线程安全的
- StringBuilder 不是线程安全的
- StringBuffer 是线程安全的,内部使用 synchronized 来同步
> [String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder)
## String 不可变的原因
**1. 可以缓存 hash 值**
因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 等情况。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
**2. String Pool 的需要**
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
<div align="center"> <img src="../pics//f76067a5-7d5f-4135-9549-8199c77d8f1c.jpg"/> </div><br>
**3. 安全性**
String 经常作为参数String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的那么在网络连接过程中String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
**4. 线程安全**
String 不可变性天生具备线程安全,可以在多个线程中使用。
> [Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/)
## String.intern()
使用 String.intern() 可以保证所有相同内容的字符串变量引用相同的内存对象。
> [揭开 String.intern() 那神秘的面纱](https://www.jianshu.com/p/95f516cb75ef)
# 六、基本类型与运算
## 包装类型
八个基本类型boolean/1 byte/8 char/16 short/16 int/32 float/32 long/64 double/64
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
```java
Integer x = 2; // 装箱
int y = x; // 拆箱
```
new Integer(123) 与 Integer.valueOf(123) 的区别在于Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。
```java
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println("a==b? " + (a == b));
Integer c = Integer.valueOf(1);
Integer d = Integer.valueOf(1);
System.out.println("c==d? " + (c == d));
}
```
```html
a==b? false
c==d? true
```
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。
```java
public static Integer valueOf(int i) {
final int offset = 128;
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + offset];
}
return new Integer(i);
}
```
基本类型中可以使用缓存池的值如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
自动装箱过程编译器会调用 valueOf() 方法,因此多个 Integer 对象使用装箱来创建并且值相同,那么就会引用相同的对象。这样做很显然是为了节省内存开销。
```java
Integer x = 1;
Integer y = 1;
System.out.println(c == d); // true
```
> [Differences between new Integer(123), Integer.valueOf(123) and just 123
](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123)
## switch
A switch works with the byte, short, char, and int primitive data types. It also works with enumerated types and a few special classes that "wrap" certain primitive types: Character, Byte, Short, and Integer.
In the JDK 7 release, you can use a String object in the expression of a switch statement.
switch 不支持 long是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
> [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java)
switch 使用查找表的方式来实现JVM 中使用的指令是 lookupswitch。
```java
public static void main(String... args) {
switch (1) {
case 1:
break;
case 2:
break;
}
}
public static void main(java.lang.String[]);
Code:
Stack=1, Locals=1, Args_size=1
0: iconst_1
1: lookupswitch{ //2
1: 28;
2: 31;
default: 31 }
28: goto 31
31: return
```
> [How does Java's switch work under the hood?](https://stackoverflow.com/questions/12020048/how-does-javas-switch-work-under-the-hood)
# 七、反射
每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName('com.mysql.jdbc.Driver.class') 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
Class 和 java.lang.reflect 一起对反射提供了支持java.lang.reflect 类库主要包含了以下三个类:
1. **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字;
2. **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法;
3. **Constructor** :可以用 Constructor 创建新的对象。
IDE 使用反射机制获取类的信息,在使用一个类的对象时,能够把类的字段、方法和构造函数等信息列出来供用户选择。
> [深入解析 Java 反射1- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/)
**Advantages of Using Reflection:**
- **Extensibility Features** : An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names.
- **Class Browsers and Visual Development Environments** : A class browser needs to be able to enumerate the members of classes. Visual development environments can benefit from making use of type information available in reflection to aid the developer in writing correct code.
- **Debuggers and Test Tools** : Debuggers need to be able to examine private members on classes. Test harnesses can make use of reflection to systematically call a discoverable set APIs defined on a class, to insure a high level of code coverage in a test suite.
**Drawbacks of Reflection:**
Reflection is powerful, but should not be used indiscriminately. If it is possible to perform an operation without using reflection, then it is preferable to avoid using it. The following concerns should be kept in mind when accessing code via reflection.
- **Performance Overhead** : Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
- **Security Restrictions** : Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet.
- **Exposure of Internals** :Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform.
> [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html)
# 八、异常
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error****Exception**,其中 Error 用来表示编译时系统错误。
Exception 分为两种:
1. **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
2. **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception此时程序奔溃并且无法恢复。
<div align="center"> <img src="../pics//48f8f98e-8dfd-450d-8b5b-df4688f0d377.jpg"/> </div><br>
> - [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception)
> - [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html)
# 九、泛型
```java
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
```
> - [Java 泛型详解](https://www.ziwenxie.site/2017/03/01/java-generic/)
> - [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
# 十、注解
Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
> [注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html)
# 十一、特性
## 面向对象三大特性
> [封装、继承、多态](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%80%9D%E6%83%B3.md#%E5%B0%81%E8%A3%85%E7%BB%A7%E6%89%BF%E5%A4%9A%E6%80%81)
## Java 各版本的新特性
**New highlights in Java SE 8**
1. Lambda Expressions
2. Pipelines and Streams
3. Date and Time API
4. Default Methods
5. Type Annotations
6. Nashhorn JavaScript Engine
7. Concurrent Accumulators
8. Parallel operations
9. PermGen Error Removed
**New highlights in Java SE 7**
1. Strings in Switch Statement
2. Type Inference for Generic Instance Creation
3. Multiple Exception Handling
4. Support for Dynamic Languages
5. Try with Resources
6. Java nio Package
7. Binary Literals, Underscore in literals
8. Diamond Syntax
> - [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17)
> - [Java 8 特性 ](http://www.importnew.com/19345.html)
## Java 与 C++ 的区别
Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.ObjectC++ 为了兼容 C 即支持面向对象也支持面向过程。
| Java | C++ |
| -- | -- |
| Java does not support pointers, templates, unions, operator overloading, structures etc. The Java language promoters initially said "No pointers!", but when many programmers questioned how you can work without pointers, the promoters began saying "Restricted pointers." Java supports what it calls "references". References act a lot like pointers in C++ languages but you cannot perform arithmetic on pointers in Java. References have types, and they're type-safe. These references cannot be interpreted as raw address and unsafe conversion is not allowed. | C++ supports structures, unions, templates, operator overloading, pointers and pointer arithmetic.|
| Java support automatic garbage collection. It does not support destructors as C++ does. | C++ support destructors, which is automatically invoked when the object is destroyed. |
| Java does not support conditional compilation and inclusion. | Conditional inclusion (#ifdef #ifndef type) is one of the main features of C++. |
| Java has built in support for threads. In Java, there is a `Thread` class that you inherit to create a new thread and override the `run()` method. | C++ has no built in support for threads. C++ relies on non-standard third-party libraries for thread support. |
| Java does not support default arguments. There is no scope resolution operator (::) in Java. The method definitions must always occur within a class, so there is no need for scope resolution there either. | C++ supports default arguments. C++ has scope resolution operator (::) which is used to to define a method outside a class and to access a global variable within from the scope where a local variable also exists with the same name. |
| There is no _goto_ statement in Java. The keywords `const` and `goto` are reserved, even though they are not used. | C++ has _goto_ statement. However, it is not considered good practice to use of _goto_ statement. |
| Java doesn't provide multiple inheritance, at least not in the same sense that C++ does. | C++ does support multiple inheritance. The keyword `virtual` is used to resolve ambiguities during multiple inheritance if there is any. |
| Exception handling in Java is different because there are no destructors. Also, in Java, try/catch must be defined if the function declares that it may throw an exception. | While in C++, you may not include the try/catch even if the function throws an exception. |
| Java has method overloading, but no operator overloading. The `String` class does use the `+` and `+=` operators to concatenate strings and `String`expressions use automatic type conversion, but that's a special built-in case. | C++ supports both method overloading and operator overloading. |
| Java has built-in support for documentation comments (`/** ... */`); therefore, Java source files can contain their own documentation, which is read by a separate tool usually `javadoc` and reformatted into HTML. This helps keeping documentation maintained in easy way. | C++ does not support documentation comments. |
| Java is interpreted for the most part and hence platform independent. | C++ generates object code and the same code may not run on different platforms. |
> [What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php)
## JRE or JDK
- JRE is the JVM program, Java application need to run on JRE.
- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac"
# 参考资料
- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002.
- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017.

755
notes/Java 容器.md Normal file
View File

@ -0,0 +1,755 @@
<!-- GFM-TOC -->
* [一、概览](#一概览)
* [Collection](#collection)
* [Map](#map)
* [二、容器中的设计模式](#二容器中的设计模式)
* [迭代器模式](#迭代器模式)
* [适配器模式](#适配器模式)
* [三、散列](#三散列)
* [四、源码分析](#四源码分析)
* [ArrayList](#arraylist)
* [Vector](#vector)
* [LinkedList](#linkedlist)
* [TreeMap](#treemap)
* [HashMap](#hashmap)
* [LinkedHashMap](#linkedhashmap)
* [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17)
* [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18)
* [五、参考资料](#五参考资料)
<!-- GFM-TOC -->
# 一、概览
容器主要包括 Collection 和 Map 两种Collection 又包含了 List、Set 以及 Queue。
## Collection
<div align="center"> <img src="../pics//java-collections.png"/> </div><br>
### 1. Set
- HashSet基于哈希实现支持快速查找但不支持有序性操作例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- TreeSet基于红黑树实现支持有序性操作但是查找效率不如 HashSetHashSet 查找时间复杂度为 O(1)TreeSet 则为 O(logn)
- LinkedHashSet具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。
### 2. List
- ArrayList基于动态数组实现支持随机访问
- Vector和 ArrayList 类似,但它是线程安全的;
- LinkedList基于双向循环链表实现只能顺序访问但是可以快速地在链表中间插入和删除元素。不仅如此LinkedList 还可以用作栈、队列和双端队列。
### 3. Queue
- LinkedList可以用它来支持双向队列
- PriorityQueue 是基于堆结构实现,可以用它来实现优先级队列。
## Map
<div align="center"> <img src="../pics//java-collections1.png"/> </div><br>
- HashMap基于哈希实现
- HashTable和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
- LinkedHashMap使用链表来维护元素的顺序顺序为插入顺序或者最近最少使用LRU顺序。
- TreeMap基于红黑树实现。
# 二、容器中的设计模式
## 迭代器模式
<div align="center"> <img src="../pics//Iterator-1.jpg"/> </div><br>
Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。
```java
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
System.out.println(item);
}
```
> [迭代器模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%8D%81%E4%BA%8C%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F)
## 适配器模式
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
```java
List list = Arrays.asList(1, 2, 3);
int[] arr = {1, 2, 3};
list = Arrays.asList(arr);
```
> [适配器模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%8D%81%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F)
# 三、散列
hasCode() 返回散列值,使用的是对象的地址。
而 equals() 是用来判断两个对象是否相等的,相等的两个对象散列值一定要相同,但是散列值相同的两个对象不一定相等。
相等必须满足以下五个性质:
**1. 自反性**
```java
x.equals(x); // true
```
**2. 对称性**
```java
x.equals(y) == y.equals(x) // true
```
**3. 传递性**
```java
if(x.equals(y) && y.equals(z)) {
x.equals(z); // true;
}
```
**4. 一致性**
多次调用 equals() 方法结果不变
```java
x.equals(y) == x.equals(y); // true
```
**5. 与 null 的比较**
对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
```java
x.euqals(null); // false;
```
# 四、源码分析
建议先阅读 [算法-查找](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E6%9F%A5%E6%89%BE) 部分,对容器类源码的理解有很大帮助。
至于 ConcurrentHashMap 的理解,需要有并发方面的知识,建议先阅读:[Java 并发](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md)
以下源码从 JDK 1.8 提取而来,下载地址:[JDK-Source-Code](https://github.com/CyC2018/JDK-Source-Code)。
## ArrayList
[ArraList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java)
### 1. 概览
实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。
```java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
```
基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。这是 ArrayList 具有动态扩容特性因此保存元素的数组不一定都会被使用那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那么部分内容。
```java
transient Object[] elementData; // non-private to simplify nested class access
```
数组的默认大小为 10。
```java
private static final int DEFAULT_CAPACITY = 10;
```
删除元素时需要调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高。
```java
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
```
添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,使得新容量为旧容量的 1.5 倍oldCapacity + (oldCapacity >> 1))。扩容操作需要把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
```java
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
```
### 2. Fail-Fast
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。
```java
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
```
### 3. 和 Vector 的区别
- Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector因为同步操作完全可以由程序员自己来控制
- Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
为了获得线程安全的 ArrayList可以调用 Collections.synchronizedList(new ArrayList<>()); 返回一个线程安全的 ArrayList也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类;
### 4. 和 LinkedList 的区别
- ArrayList 基于动态数组实现LinkedList 基于双向循环链表实现;
- ArrayList 支持随机访问LinkedList 不支持;
- LinkedList 在任意位置添加删除元素更快。
## Vector
[Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java)
## LinkedList
[LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java)
## TreeMap
[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java)
## HashMap
[HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
### 1. 存储结构
使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table数组中的每个位置被当成一个桶。
```java
transient Entry[] table;
```
其中Entry 就是存储数据的键值对,它包含了四个字段。从 next 字段我们可以看出 Entry 是一个链表,即每个桶会存放一个链表。
<div align="center"> <img src="../pics//ce039f03-6588-4f0c-b35b-a494de0eac47.png" width="500"/> </div><br>
JDK 1.8 使用 Node 类型存储一个键值对,它依然继承自 Entry因此可以按照上面的存储结构来理解。
需要注意的是Key 类型为 final这意味着它不可改变因此每个桶的链表采用头插法实现也就是说新节点需要只能在链表头部插入。
```java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
```
### 2. 拉链法的工作原理
```java
HashMap<String, Integer> map = new HashMap<>(); // 默认大小为 16
map.put("sachin", 30);
map.put("vishal", 20);
map.put("vaibhav", 20);
```
- 计算 "sachin" 的 hashcode 为 115使用除留余数法得到 115 % 16 = 3因此 ("sachin", 30) 键值对放到第 3 个桶上。
- 同样得到 ("vishal", 20) 和 ("vaibhav", 20) 都应该放到第 6 个桶上。("vishal", 20) 先放入, ("vaibhav", 20) 链接到 ("vishal", 20) 之后。
<div align="center"> <img src="../pics//b9a39d2a-618c-468b-86db-2e851f1a0057.jpg" width="600"/> </div><br>
当进行查找时,需要分成两步进行,第一步是先根据 hashcode 计算出所在的桶,第二步是在链表上顺序查找。由于 table 是数组形式的,具有随机读取的特性,因此第一步的时间复杂度为 O(1),而第二步需要在链表上顺序查找,时间复杂度显然和链表的长度成正比。
### 3. 链表转红黑树
应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
<div align="center"> <img src="../pics//061c29ce-e2ed-425a-911e-56fbba1efce3.jpg" width="500"/> </div><br>
### 4. 扩容
因为从 JDK 1.8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 JDK 1.7 的内容。
设 HashMap 的 table 长度为 M需要存储的键值对数量为 N如果哈希函数满足均匀性的要求那么每条链表的长度大约为 N/M因此平均查找次数的数量级为 O(N/M)。
为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,可就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
和扩容相关的参数主要有capacity、size、threshold 和 load_factor。
| 参数 | 含义 |
| :--: | :-- |
| capacity | table 的容量大小,默认为 16需要注意的是 capacity 必须保证为 2 的次方。|
| size | table 的实际使用量。 |
| threshold | size 的临界值size 必须小于 threshold如果大于等于就必须进行扩容操作。 |
| load_factor | table 能够使用的比例threshold = capacity * load_factor。|
```java
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;
```
从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。
```java
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
```
扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把旧 table 的所有键值对重新插入新的 table 中,因此这一步是很费时的。
```java
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
```
### 5. 确定桶下标
需要三步操作:计算 Key 的 hashCode、高位运算、除留余数法取模。
<div align="center"> <img src="../pics//hashMap_u54C8_u5E0C_u7B97_u6CD5_u4F8B_u56FE.png" width="800"/> </div><br>
**hashcode()**
```java
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
```
**(二)高位运算**
通过 hashCode() 的高 16 位异或低 16 位,使得数组比较小时,也能保证高低位都参与到了哈希计算中。
```java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
```
**(三)除留余数**
令 x = 1<<4 x 2 4 次方它具有以下性质
```
x : 00010000
x-1 : 00001111
```
令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位及以上数:
```
y : 10110010
x-1 : 00001111
y&(x-1) : 00000010
```
这个性质和 y 对 x 取模效果是一样的:
```
x : 00010000
y : 10110010
y%x : 00000010
```
我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。
拉链法需要使用除留余数法来得到桶下标也就是需要进行以下计算hash%capacity如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。
以下操作在 Java 8 中没有,但是原理上相同。
```java
static int indexFor(int h, int length) {
return h & (length-1);
}
```
### 6. 扩容-重新计算桶下标
在进行扩容时,需要把 Node 重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。
假设原数组长度 capacity 为 8扩容之后 new capacity 为 16
```html
capacity : 00010000
new capacity : 00100000
```
对于一个 Key它的 hashCode 如果在第 6 位上为 0那么除留余数得到的结果和之前一样如果为 1那么得到的结果为原来的结果 + 8。
### 7. 扩容-计算数组容量
先考虑如何求一个数的补码,对于 10100000它的补码为 11111111可以使用以下方法得到
```
mask |= mask >> 1 11000000
mask |= mask >> 2 11110000
mask |= mask >> 4 11111111
```
如果最后令 mask+1得到就是大于原始数字的最小的 2 次方。
以下是 HashMap 中计算一个大小所需要的数组容量的代码:
```java
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
```
### 7. null 值
HashMap 允许有一个 Node 的 Key 为 null该 Node 一定会放在第 0 个桶的位置,因为这个 Key 无法计算 hashCode(),因此只能规定一个桶让它存放。
### 8. 与 HashTable 的区别
- HashTable 是同步的,它使用了 synchronized 来进行同步。它也是线程安全的,多个线程可以共享同一个 HashTable。HashMap 不是同步的,但是可以使用 ConcurrentHashMap它是 HashTable 的替代,而且比 HashTable 可扩展性更好。
- HashMap 可以插入键为 null 的 Entry。
- HashMap 的迭代器是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。
- 由于 Hashtable 是线程安全的也是 synchronized所以在单线程环境下它比 HashMap 要慢。
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
## LinkedHashMap
[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
## ConcurrentHashMap - JDK 1.7
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java)
ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。
相比于 HashTable 和用同步包装器包装的 HashMapCollections.synchronizedMap(new HashMap())ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
### 1. 存储结构
和 HashMap 类似。
```java
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
```
继承自 ReentrantLock每个 Segment 维护着多个 HashEntry。
```java
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
```
```java
final Segment<K,V>[] segments;
```
默认的并发级别为 16也就是说默认创建 16 个 Segment。
```java
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
```
<div align="center"> <img src="../pics//image005.jpg"/> </div><br>
### 2. HashEntery 的不可变性
HashEntry 中的 keyhashnext 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。
同时HashEntry 类的 value 域被声明为 Volatile 型Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。
```java
final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
```
在以下链表中删除 C 节点C 节点之后的所有节点都原样保留C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。
<div align="center"> <img src="../pics//image007.jpg"/> </div><br>
<div align="center"> <img src="../pics//image008.jpg"/> </div><br>
除了 remove 操作,其它操作也类似。可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。
### 3. Volatile 变量
```java
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
```
由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。
下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。
<div align="center"> <img src="../pics//image009.jpg"/> </div><br>
假设线程 M 在写入了 volatile 型变量 count 后,线程 N 读取了这个 volatile 型变量 count。
根据 happens-before 关系法则中的程序次序法则A appens-before 于 BC happens-before D。
根据 Volatile 变量法则B happens-before C。
根据传递性,连接上面三个 happens-before 关系得到A appens-before 于 B B appens-before CC happens-before D。也就是说写线程 M 对链表做的结构性修改,在读线程 N 读取了同一个 volatile 变量后,对线程 N 也是可见的了。
虽然线程 N 是在未加锁的情况下访问链表。Java 的内存模型可以保证:只要之前对链表做结构性修改操作的写线程 M 在退出写方法前写 volatile 型变量 count读线程 N 在读取这个 volatile 型变量 count 后,就一定能 “看到” 这些修改。
ConcurrentHashMap 中,每个 Segment 都有一个变量 count。它用来统计 Segment 中的 HashEntry 的个数。这个变量被声明为 volatile。
```java
transient volatile int count;
```
所有不加锁读方法,在进入读方法时,首先都会去读这个 count 变量。比如下面的 get 方法:
```java
V get(Object key, int hash) {
if(count != 0) { // 首先读 count 变量
HashEntry<K,V> e = getFirst(hash);
while(e != null) {
if(e.hash == hash && key.equals(e.key)) {
V v = e.value;
if(v != null)
return v;
// 如果读到 value 域为 null说明发生了重排序加锁后重新读取
return readValueUnderLock(e);
}
e = e.next;
}
}
return null;
}
```
在 ConcurrentHashMap 中所有执行写操作的方法put, remove, clear在对链表做结构性修改之后在退出写方法前都会去写这个 count 变量。所有未加锁的读操作get, contains, containsKey在读方法中都会首先去读取这个 count 变量。
根据 Java 内存模型,对 同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。
这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。
### 4. 小结
ConcurrentHashMap 的高并发性主要来自于三个方面:
- 用分离锁实现多个线程间的更深层次的共享访问。
- 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
- 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
## ConcurrentHashMap - JDK 1.8
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java)
<div align="center"> <img src="../pics//7779232-1e8ed39548081a1f.png"/> </div><br>
JDK 1.7 分段锁机制来实现并发更新操作,核心类为 Segment它继承自重入锁 ReentrantLock。
<div align="center"> <img src="../pics//7779232-96822582feb08651.png"/> </div><br>
JDK 1.8 的实现不是用了 SegmentSegment 属于重入锁 ReentrantLock。而是使用了内置锁 synchronized主要是出于以下考虑
1. synchronized 的锁粒度更低;
2. synchronized 优化空间更大;
3. 在大量数据操作的情况下ReentrantLock 会开销更多的内存。
并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
# 五、参考资料
- Java 编程思想
- [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php)
- [Iterator 模式](https://openhome.cc/Gossip/DesignPattern/IteratorPattern.htm)
- [Java 8 系列之重新认识 HashMap](https://tech.meituan.com/java-hashmap.html)
- [What is difference between HashMap and Hashtable in Java?](http://javarevisited.blogspot.hk/2010/10/difference-between-hashmap-and.html)
- [Java 集合之 HashMap](http://www.zhangchangle.com/2018/02/07/Java%E9%9B%86%E5%90%88%E4%B9%8BHashMap/)
- [The principle of ConcurrentHashMap analysis](http://www.programering.com/a/MDO3QDNwATM.html)
- [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
- [HashMap 相关面试题及其解答](https://www.jianshu.com/p/75adf47958a7)

930
notes/Java 并发.md Normal file
View File

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

686
notes/Java 虚拟机.md Normal file
View File

@ -0,0 +1,686 @@
<!-- GFM-TOC -->
* [一、运行时数据区域](#一运行时数据区域)
* [程序计数器](#程序计数器)
* [Java 虚拟机栈](#java-虚拟机栈)
* [本地方法栈](#本地方法栈)
* [Java 堆](#java-堆)
* [方法区](#方法区)
* [运行时常量池](#运行时常量池)
* [直接内存](#直接内存)
* [二、垃圾收集](#二垃圾收集)
* [判断一个对象是否可回收](#判断一个对象是否可回收)
* [垃圾收集算法](#垃圾收集算法)
* [垃圾收集器](#垃圾收集器)
* [内存分配与回收策略](#内存分配与回收策略)
* [Full GC 的触发条件](#full-gc-的触发条件)
* [三、类加载机制](#三类加载机制)
* [类的生命周期](#类的生命周期)
* [类初始化时机](#类初始化时机)
* [类加载过程](#类加载过程)
* [类加载器](#类加载器)
* [四、JVM 参数](#四jvm-参数)
* [GC 优化配置](#gc-优化配置)
* [GC 类型设置](#gc-类型设置)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、运行时数据区域
<div align="center"> <img src="../pics//dc695f48-4189-4fc7-b950-ed25f6c1521708518830.jpg"/> </div><br>
注:白色区域为线程私有,蓝色区域为线程共享。
## 程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
## Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小:
```java
java -Xss=512M HackTheJava
```
该区域可能抛出以下异常:
1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
2. 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
## 本地方法栈
本地方法不是用 Java 实现,对待这些方法需要特别处理。
与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
## Java 堆
所有对象实例都在这里分配内存。
是垃圾收集的主要区域("GC 堆 "),现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把 Java 堆分成以下三块:
- 新生代Young Generation
- 老年代Old Generation
- 永久代Permanent Generation
当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:
- Eden
- From Survivor
- To Survivor
<div align="center"> <img src="../pics//ppt_img.gif"/> </div><br>
Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置最小值,第二个参数设置最大值。
```java
java -Xms=1M -XmX=2M HackTheJava
```
## 方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载但是一般比较难实现HotSpot 虚拟机把它当成永久代来进行垃圾回收。
## 运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。
## 直接内存
在 JDK 1.4 中新加入了 NIO 类引入了一种基于通道Channel与缓冲区Buffer的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
# 二、垃圾收集
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。
## 判断一个对象是否可回收
### 1. 引用计数
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1引用失效时计数器减 1。引用计数为 0 的对象可被回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0导致无法对它们进行回收。
```java
objA.instance = objB;
objB.instance = objA;
```
### 2. 可达性
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是都是可用的,不可达的对象可被回收。
GC Roots 一般包含以下内容:
1. 虚拟机栈中引用的对象
2. 方法区中类静态属性引用的对象
3. 方法区中的常量引用的对象
4. 本地方法栈中引用的对象
### 3. 引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
Java 对引用的概念进行了扩充,引入四种强度不同的引用类型。
**(一)强引用**
只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。
使用 new 一个新对象的方式来创建强引用。
```java
Object obj = new Object();
```
**(二)软引用**
用来描述一些还有用但是并非必需的对象。
在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出溢出异常。
软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。
使用 SoftReference 类来实现软引用。
```java
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
```
**(三)弱引用**
只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会被回收。
使用 WeakReference 类来实现弱引用。
```java
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
```
**(四)虚引用**
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
```java
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
```
### 4. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2. 加载该类的 ClassLoader 已经被回收。
3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGo 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
### 5. finalize()
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。
## 垃圾收集算法
### 1. 标记 - 清除
<div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg"/> </div><br>
将需要回收的对象进行标记,然后清除。
不足:
1. 标记和清除过程效率都不高
2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存
之后的算法都是基于该算法进行改进。
### 2. 复制
<div align="center"> <img src="../pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg"/> </div><br>
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。
### 3. 标记 - 整理
<div align="center"> <img src="../pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg"/> </div><br>
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
### 4. 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将 Java 堆分为新生代和老年代。
1. 新生代使用:复制算法
2. 老年代使用:标记 - 清理 或者 标记 - 整理 算法
## 垃圾收集器
<div align="center"> <img src="../pics//c625baa0-dde6-449e-93df-c3a67f2f430f.jpg"/> </div><br>
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
### 1. Serial 收集器
<div align="center"> <img src="../pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg"/> </div><br>
它是单线程的收集器,不仅意味着只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停所有其他工作线程,往往造成过长的等待时间。
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
在 Client 应用场景中,分配给虚拟机管理的内存一般来说不会很大,该收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
### 2. ParNew 收集器
<div align="center"> <img src="../pics//81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg"/> </div><br>
它是 Serial 收集器的多线程版本。
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
### 3. Parallel Scavenge 收集器
是并行的多线程收集器。
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
还提供了一个参数 -XX:+UseAdaptiveSizePolicy这是一个开关参数打开参数后就不需要手工指定新生代的大小-Xmn、Eden 和 Survivor 区的比例(-XX:SurvivorRatio、晋升老年代对象年龄-XX:PretenureSizeThreshold等细节参数了虚拟机会根据当前系统的运行情况收集性能监控信息动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量这种方式称为 GC 自适应的调节策略GC Ergonomics。自适应调节策略也是它与 ParNew 收集器的一个重要区别。
### 4. Serial Old 收集器
<div align="center"> <img src="../pics//08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg"/> </div><br>
Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
1. 在 JDK 1.5 以及之前版本Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
2. 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
### 5. Parallel Old 收集器
<div align="center"> <img src="../pics//278fe431-af88-4a95-a895-9c3b80117de3.jpg"/> </div><br>
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
### 6. CMS 收集器
<div align="center"> <img src="../pics//62e77997-6957-4b68-8d12-bfd609bb2c68.jpg"/> </div><br>
CMSConcurrent Mark Sweep从 Mark Sweep 可以知道它是基于标记 - 清除算法实现的。
特点:并发收集、低停顿。
分为以下四个流程:
1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
2. 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
4. 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
1. 对 CPU 资源敏感。CMS 默认启动的回收线程数是 (CPU 数量 + 3) / 4当 CPU 不足 4 个时CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。并且低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率变低。
2. 无法处理浮动垃圾。由于并发清理阶段用户线程还在运行着伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后CMS 无法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此它不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工作的内存占用百分比JDK 1.5 默认设置下该值为 68也就是当老年代使用了 68% 的空间之后会触发收集器工作。如果该值设置的太高,导致浮动垃圾无法保存,那么就会出现 Concurrent Mode Failure此时虚拟机将启动后备预案临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。
3. 标记 - 清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
### 7. G1 收集器
<div align="center"> <img src="../pics//f99ee771-c56f-47fb-9148-c0036695b5fe.jpg"/> </div><br>
G1Garbage-First收集器是当今收集器技术发展最前沿的成果之一它是一款面向服务端应用的垃圾收集器HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。
具备如下特点:
- 并行与并发:能充分利用多 CPU 环境下的硬件优势,使用多个 CPU 来缩短停顿时间。
- 分代收集:分代概念依然得以保留,虽然它不需要其它收集器配合就能独立管理整个 GC 堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次 GC 的旧对象来获取更好的收集效果。
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:这是它相对 CMS 的一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒,这几乎已经是实时 JavaRTSJ的垃圾收集器的特征了。
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老生代,而 G1 不再是这样Java 堆的内存布局与其他收集器有很大区别,将整个 Java 堆划分为多个大小相等的独立区域Region。虽然还保留新生代和老年代的概念但新生代和老年代不再是物理隔离的了而都是一部分 Region不需要连续的集合。
之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。它跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了它在有限的时间内可以获取尽可能高的收集效率。
Region 不可能是孤立的,一个对象分配在某个 Region 中,可以与整个 Java 堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个 Java 堆才能保证准确性,这显然是对 GC 效率的极大伤害。为了避免全堆扫描的发生,每个 Region 都维护了一个与之对应的 Remembered Set。虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中,如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。
如果不计算维护 Remembered Set 的操作G1 收集器的运作大致可划分为以下几个步骤:
1. 初始标记
2. 并发标记
3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region时间是用户可控制的而且停顿用户线程将大幅度提高收集效率。
### 8. 七种垃圾收集器的比较
| 收集器 | 串行、并行 or 并发 | 新生代 / 老年代 | 算法 | 目标 | 适用场景 |
| :---: | :---: | :---: | :---: | :---: | :---: |
| **Serial** | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
| **ParNew** | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
| **Parallel Scavenge** | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **Parallel Old** | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **CMS** | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
| **G1** | 并发 | both | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
## 内存分配与回收策略
对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。
### 1. 优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
关于 Minor GC 和 Full GC
- Minor GC发生在新生代上因为新生代对象存活时间很短因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC发生在老年代上老年代对象和新生代的相反其存活时间长因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。
### 2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
### 3. 长期存活的对象进入老年代
JVM 为对象定义年龄计数器,经过 Minor GC 依然存活,并且能被 Survivor 区容纳的,移被移到 Survivor 区,年龄就增加 1 岁,增加到一定年龄则移动到老年代中(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置)。
### 4. 动态对象年龄判定
JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无序等待 MaxTenuringThreshold 中要求的年龄。
### 5. 空间分配担保
在发生 Minor GC 之前JVM 先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话 JVM 会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
## Full GC 的触发条件
对于 Minor GC其触发条件非常简单当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
### 1. 调用 System.gc()
此方法的调用是建议 JVM 进行 Full GC虽然只是建议而非一定但很多情况下它会触发 Full GC从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。
### 2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。
### 3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。
### 4. JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError为避免以上原因引起的 Full GC可采用的方法为增大永久代空间或转为使用 CMS GC。
### 5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC便会报 Concurrent Mode Failure 错误,并触发 Full GC。
# 三、类加载机制
类是在运行期间动态加载的。
## 类的生命周期
<div align="center"> <img src="../pics//32b8374a-e822-4720-af0b-c0f485095ea2.jpg"/> </div><br>
包括以下 7 个阶段:
- **加载Loading**
- **验证Verification**
- **准备Preparation**
- **解析Resolution**
- **初始化Initialization**
- 使用Using
- 卸载Unloading
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
## 类初始化时机
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随着发生):
1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
5. 当使用 JDK.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类初始化。
```java
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
```
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
```java
SuperClass[] sca = new SuperClass[10];
```
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
```java
System.out.println(ConstClass.HELLOWORLD);
```
## 类加载过程
包含了加载、验证、准备、解析和初始化这 5 个阶段。
### 1. 加载
加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
1. 通过一个类的全限定名来获取定义此类的二进制字节流。
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
3. 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
- 从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。
- 从网络中获取,这种场景最典型的应用是 Applet。
- 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
- 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
...
### 2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
主要有以下 4 个阶段:
**(一)文件格式验证**
验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
**(二)元数据验证**
对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
**(三)字节码验证**
通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
**(四)符号引用验证**
发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
### 3. 准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
```java
public static int value = 123;
```
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
```java
public static final int value = 123;
```
### 4. 解析
将常量池的符号引用替换为直接引用的过程。
### 5. 初始化
初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 &lt;clinit>() 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
&lt;clinit>() 方法具有以下特点:
- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
```java
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
```
- 与类的构造函数(或者说实例构造器 &lt;init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 &lt;clinit>() 方法运行之前,父类的 &lt;clinit>() 方法已经执行结束。因此虚拟机中第一个执行 &lt;clinit>() 方法的类肯定为 java.lang.Object。
- 由于父类的 &lt;clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码:
```java
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值 ,也就是 2。
}
```
- &lt;clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 &lt;clinit>() 方法。
- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 &lt;clinit>() 方法。但接口与类不同的是,执行接口的 &lt;clinit>() 方法不需要先执行父接口的 &lt;clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 &lt;clinit>() 方法。
- 虚拟机会保证一个类的 &lt;clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 &lt;clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 &lt;clinit>() 方法完毕。如果在一个类的 &lt;clinit>() 方法中有耗时的操作,就可能造成多个进程阻塞,在实际过程中此种阻塞很隐蔽。
## 类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流 ( 即字节码 )”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
### 1. 类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字做对象所属关系判定等情况),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
### 2. 类加载器分类
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
- 启动类加载器Bootstrap ClassLoader这个类加载器用 C++ 实现,是虚拟机自身的一部分;
- 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
- 启动类加载器Bootstrap ClassLoader 此类加载器负责将存放在 &lt;JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器Extension ClassLoader 这个类加载器是由 ExtClassLoadersun.misc.Launcher$ExtClassLoader实现的。它负责将 &lt;JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
- 应用程序类加载器Application ClassLoader 这个类加载器是由 AppClassLoadersun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值因此一般称为系统类加载器。它负责加载用户类路径ClassPath上所指定的类库开发者可以直接使用这个类加载器如果应用程序中没有自定义过自己的类加载器一般情况下这个就是程序中默认的类加载器。
### 3. 双亲委派模型
应用程序都是由三种类加载器相互配合进行加载的如果有必要还可以加入自己定义的类加载器。下图展示的类加载器之间的层次关系称为类加载器的双亲委派模型Parents Delegation Model。该模型要求除了顶层的启动类加载器外其余的类加载器都应有自己的父类加载器这里类加载器之间的父子关系一般通过组合Composition关系来实现而不是通过继承Inheritance的关系实现。
<div align="center"> <img src="../pics//2cdc3ce2-fa82-4c22-baaa-000c07d10473.jpg"/> </div><br>
**(一)工作过程**
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
**(二)好处**
使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object它存放在 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反如果没有双亲委派模型由各个类加载器自行加载的话如果用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。
**(三)实现**
```java
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//check the class has been loaded or not
Class c = findLoadedClass(name);
if(c == null) {
try{
if(parent != null) {
c = parent.loadClass(name, false);
} else{
c = findBootstrapClassOrNull(name);
}
} catch(ClassNotFoundException e) {
//if throws the exception , the father can not complete the load
}
if(c == null) {
c = findClass(name);
}
}
if(resolve) {
resolveClass(c);
}
return c;
}
```
# 四、JVM 参数
## GC 优化配置
| 配置 | 描述 |
| --- | --- |
| -Xms | 初始化堆内存大小 |
| -Xmx | 堆内存最大值 |
| -Xmn | 新生代大小 |
| -XX:PermSize | 初始化永久代大小 |
| -XX:MaxPermSize | 永久代最大容量 |
## GC 类型设置
| 配置 | 描述 |
| --- | --- |
| -XX:+UseSerialGC | 串行垃圾回收器 |
| -XX:+UseParallelGC | 并行垃圾回收器 |
| -XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 |
| -XX:ParallelCMSThreads= | 并发标记扫描垃圾回收器 = 为使用的线程数量 |
| -XX:+UseG1GC | G1 垃圾回收器 |
```java
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
```
# 参考资料
- 深入理解 Java 虚拟机
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)

5291
notes/Leetcode 题解.md Normal file

File diff suppressed because it is too large Load Diff

1398
notes/Linux.md Normal file

File diff suppressed because it is too large Load Diff

400
notes/MySQL.md Normal file
View File

@ -0,0 +1,400 @@
<!-- GFM-TOC -->
* [一、存储引擎](#一存储引擎)
* [InnoDB](#innodb)
* [MyISAM](#myisam)
* [比较](#比较)
* [二、数据类型](#二数据类型)
* [整型](#整型)
* [浮点数](#浮点数)
* [字符串](#字符串)
* [时间和日期](#时间和日期)
* [三、索引](#三索引)
* [索引分类](#索引分类)
* [索引的优点](#索引的优点)
* [索引优化](#索引优化)
* [B-Tree 和 B+Tree 原理](#b-tree-和-b+tree-原理)
* [四、查询性能优化](#四查询性能优化)
* [五、分库与分表](#五分库与分表)
* [原因](#原因)
* [实现方式](#实现方式)
* [Merge 存储引擎](#merge-存储引擎)
* [存在的问题](#存在的问题)
* [分表与分区的不同](#分表与分区的不同)
* [六、故障转移和故障恢复](#六故障转移和故障恢复)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、存储引擎
## InnoDB
InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。
采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读。
表是基于聚簇索引建立的,它对主键的查询性能有很高的提升。
内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够自动在内存中创建哈希索引以加速读操作的自适应哈希索引、能够加速插入操作的插入缓冲区等。
通过一些机制和工具支持真正的热备份。
## MyISAM
MyISAM 提供了大量的特性包括全文索引、压缩、空间函数GIS等。但 MyISAM 不支持事务和行级锁,而且崩溃后无法安全恢复。
只能对整张表加锁,而不是针对行。
可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
可以包含动态或者静态的行。
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表适合采用 MyISAM 压缩表。
对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。
MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性能很好。
## 比较
1. 事务InnoDB 是事务型的。
2. 备份InnoDB 支持在线热备份。
3. 崩溃恢复MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
4. 并发MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
5. 其它特性MyISAM 支持全文索引,地理空间索引。
# 二、数据类型
## 整型
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。
INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
## 浮点数
FLOAT 和 DOUBLE 为浮点类型DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
## 字符串
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长当超出一个页所能容纳的大小时就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
## 时间和日期
MySQL 提供了两种相似的日期时间类型DATATIME 和 TIMESTAMP。
### 1. DATATIME
能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
它与时区无关。
默认情况下MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值例如“2008-01-16 22:37:08”这是 ANSI 标准定义的日期和时间表示方法。
### 2. TIMESTAMP
和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。
它和时区有关。
MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
应该尽量使用 TIMESTAMP因为它比 DATETIME 空间效率更高。
# 三、索引
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
索引能够轻易将查询性能提升几个数量级。
对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和使用索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
## 索引分类
### 1. B-Tree 索引
B-Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。
可以指定多个列作为索引列多个索引列共同组成键。B-Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。
除了用于查找,还可以用于排序和分组。
如果不是按照索引列的顺序进行查找,则无法使用索引。
### 2. 哈希索引
基于哈希表实现,优点是查找非常快。
在 MySQL 中只有 Memory 引擎显式支持哈希索引。
InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B-Tree 索引之上再创建一个哈希索引,这样就让 B-Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。
### 3. 空间索引R-Tree
MyISAM 存储引擎支持空间索引,可以用于地理数据存储。
空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
### 4. 全文索引
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。
使用 MATCH AGAINST而不是普通的 WHERE。
## 索引的优点
- 大大减少了服务器需要扫描的数据量;
- 帮助服务器避免进行排序和创建临时表;
- 将随机 I/O 变为顺序 I/O。
## 索引优化
### 1. 独立的列
在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。
例如下面的查询不能使用 actor_id 列的索引:
```sql
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
```
### 2. 前缀索引
对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
对于前缀长度的选取需要根据 **索引选择性** 来确定:不重复的索引值和记录总数的比值。选择性越高,查询效率也越高。最大值为 1 ,此时每个记录都有唯一的索引与其对应。
### 3. 多列索引
在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。
```sql
SELECT film_id, actor_ id FROM sakila.film_actor
WhERE actor_id = 1 AND film_id = 1;
```
### 4. 索引列的顺序
让选择性最强的索引列放在前面,例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
```sql
SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM payment;
```
```html
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16049
```
### 5. 聚簇索引
<div align="center"> <img src="../pics//b9e9ae8c-e216-4c01-b267-a50dbeb98fa4.jpg"/> </div><br>
聚簇索引并不是一种索引类型,而是一种数据存储方式。
术语“聚簇”表示数据行和相邻的键值紧密地存储在一起InnoDB 的聚簇索引的数据行存放在 B-Tree 的叶子页中。
因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
#### 优点
1. 可以把相关数据保存在一起,减少 I/O 操作;
2. 因为数据保存在 B-Tree 中,因此数据访问更快。
#### 缺点
1. 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。
2. 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。
3. 更新操作代价很高,因为每个被更新的行都会移动到新的位置。
4. 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。
5. 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。
### 6. 覆盖索引
索引包含所有需要查询的字段的值。
#### 优点
1. 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。
2. 一些存储引擎(例如 MyISAM在内存中只缓存索引而数据依赖于操作系统来缓存。因此只访问索引可以不使用系统调用通常比较费时
3. 对于 InnoDB 引擎,若二级索引能够覆盖查询,则无需访问聚簇索引。
## B-Tree 和 B+Tree 原理
### 1. B-Tree
<div align="center"> <img src="../pics//5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg"/> </div><br>
为了描述 B-Tree首先定义一条数据记录为一个二元组 [key, data]key 为记录的键data 为数据记录除 key 外的数据。
B-Tree 是满足下列条件的数据结构:
- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的;
- 一个节点中的 key 从左到右非递减排列;
- 如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null则该指针指向节点的所有 key 大于 key<sub>i</sub> 且小于 key<sub>i+1</sub>
在 B-Tree 中按 key 检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的 data否则对相应区间的指针指向的节点递归进行查找直到找到节点或找到 null 指针,前者查找成功,后者查找失败。
由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。
### 2. B+Tree
<div align="center"> <img src="../pics//63cd5b50-d6d8-4df6-8912-ef4a1dd5ba13.jpg"/> </div><br>
与 B-Tree 相比B+Tree 有以下不同点:
- 每个节点的指针上限为 2d 而不是 2d+1
- 内节点不存储 data只存储 key叶子节点不存储指针。
### 3. 带有顺序访问指针的 B+Tree
<div align="center"> <img src="../pics//1ee5f0a5-b8df-43b9-95ab-c516c54ec797.jpg"/> </div><br>
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。
### 4. 为什么使用 B-Tree 和 B+Tree
红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用 B-/+Tree 作为索引结构。
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为 4k主存和磁盘以页为单位交换数据。
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O磁盘往往不是严格按需读取而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理当一个数据被用到时其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理将一个节点的大小设为等于一个页这样每个节点只需要一次 I/O 就可以完全载入。B-Tree 中一次检索最多需要 h-1 次 I/O根节点常驻内存渐进复杂度为 O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100因此 h 非常小(通常不超过 3。而红黑树这种结构h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。
B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。
# 四、查询性能优化
### Explain
用来分析 SQL 语句,分析结果中比较重要的字段有:
- select_type : 查询类型,有简单查询、联合查询和子查询
- key : 使用的索引
- rows : 扫描的行数
### 减少返回的列
慢查询主要是因为访问了过多数据,除了访问过多行之外,也包括访问过多列。
最好不要使用 SELECT * 语句,要根据需要选择查询的列。
### 减少返回的行
最好使用 LIMIT 语句来取出想要的那些行。
还可以建立索引来减少条件语句的全表扫描。例如对于下面的语句,不使用索引的情况下需要进行全表扫描,而使用索引只需要扫描几行记录即可,使用 Explain 语句可以通过观察 rows 字段来看出这种差异。
```sql
SELECT * FROM sakila.film_actor WHERE film_id = 1;
```
### 拆分大的 DELETE 或 INSERT 语句
如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
```sql
DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
```
```sql
rows_affected = 0
do {
rows_affected = do_query(
"DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
} while rows_affected > 0
```
# 五、分库与分表
## 原因
随着时间和业务的发展,数据库中的表会越来越多,并且表中的数据量也会越来越大,那么读写操作的开销也会随着增大。
## 实现方式
### 1. 垂直切分
将表按功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立商品数据库 payDB、用户数据库 userDB 等,分别用来存储项目与商品有关的表和与用户有关的表。
### 2. 水平切分
把表中的数据按照某种规则存储到多个结构相同的表中,例如按 id 的散列值、性别等进行划分。
### 3. 切分的选择
如果数据库中的表太多,并且项目各项业务逻辑清晰,那么垂直切分是首选。
如果数据库的表不多,但是单表的数据量很大,应该选择水平切分。
## Merge 存储引擎
该存储引擎支持分表。
## 存在的问题
### 1. 事务问题
在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
### 2. 跨库跨表连接问题
在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上。这时,表的连接操作将受到限制,我们无法连接位于不同分库的表,也无法连接分表粒度不同的表,导致原本只需要一次查询就能够完成的业务需要进行多次才能完成。
### 3. 额外的数据管理负担和数据运算压力
最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。
## 分表与分区的不同
分表,就是将一张表分成多个小表,这些小表拥有不同的表名;而分区是将一张表的数据分为多个区块,这些区块可以存储在同一个磁盘上,也可以存储在不同的磁盘上,这种方式下表仍然只有一个。
# 六、故障转移和故障恢复
故障转移也叫做切换,当主库出现故障时就切换到备库,使备库成为主库。故障恢复顾名思义就是从故障中恢复过来,并且保证数据的正确性。
### 提升备库或切换角色
提升一台备库为主库,或者在一个主-主复制结构中调整主动和被动角色。
### 虚拟 IP 地址和 IP 托管
为 MySQL 实例指定一个逻辑 IP 地址,当 MySQL 实例失效时,可以将 IP 地址转移到另一台 MySQL 服务器上。
### 中间件解决方案
通过代理,可以路由流量到可以使用的服务器上。
<div align="center"> <img src="../pics//fabd5fa0-b75e-48d0-9e2c-31471945ceb9.jpg"/> </div><br>
### 在应用中处理故障转移
将故障转移整合到应用中可能导致应用变得太过笨拙。
# 参考资料
- 高性能 MySQL
- [MySQL 索引背后的数据结构及算法原理 ](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
- [20+ 条 MySQL 性能优化的最佳经验 ](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
- [数据库为什么分库分表mysql的分库分表方案](https://www.i3geek.com/archives/1108)

460
notes/Redis.md Normal file
View File

@ -0,0 +1,460 @@
<!-- GFM-TOC -->
* [一、Redis 是什么](#一redis-是什么)
* [二、五种基本类型](#二五种基本类型)
* [1. STRING](#1-string)
* [2. LIST](#2-list)
* [3. SET](#3-set)
* [4. HASH](#4-hash)
* [5. ZSET](#5-zset)
* [三、键的过期时间](#三键的过期时间)
* [四、发布与订阅](#四发布与订阅)
* [五、事务](#五事务)
* [六、持久化](#六持久化)
* [1. 快照持久化](#1-快照持久化)
* [2. AOF 持久化](#2-aof-持久化)
* [七、复制](#七复制)
* [从服务器连接主服务器的过程](#从服务器连接主服务器的过程)
* [主从链](#主从链)
* [八、处理故障](#八处理故障)
* [九、分片](#九分片)
* [1. 客户端分片](#1-客户端分片)
* [2. 代理分片](#2-代理分片)
* [3. 服务器分片](#3-服务器分片)
* [十、事件](#十事件)
* [事件类型](#事件类型)
* [事件的调度与执行](#事件的调度与执行)
* [十一、Redis 与 Memcached 的区别](#十一redis-与-memcached-的区别)
* [数据类型](#数据类型)
* [数据持久化](#数据持久化)
* [分布式](#分布式)
* [内存管理机制](#内存管理机制)
* [十二、Redis 适用场景](#十二redis-适用场景)
* [缓存](#缓存)
* [消息队列](#消息队列)
* [计数器](#计数器)
* [好友关系](#好友关系)
* [十三、数据淘汰策略](#十三数据淘汰策略)
* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
* [文章信息](#文章信息)
* [点赞功能](#点赞功能)
* [对文章进行排序](#对文章进行排序)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、Redis 是什么
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
五种类型数据类型为:字符串、列表、集合、有序集合、散列表。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
# 二、五种基本类型
| 数据类型 | 可以存储的值 | 操作 |
| -- | -- | -- |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
| LIST | 链表 | 从两端压入或者弹出元素</br> 读取单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
| ZSET | 有序集合 | 添加、获取、删除元素个元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/)
## 1. STRING
<div align="center"> <img src="../pics//6019b2db-bc3e-4408-b6d8-96025f4481d6.png" width="400"/> </div><br>
```html
> set hello world
OK
> get hello
"world"
> del hello
(integer) 1
> get hello
(nil)
```
## 2. LIST
<div align="center"> <img src="../pics//fb327611-7e2b-4f2f-9f5b-38592d408f07.png" width="400"/> </div><br>
```html
> rpush list-key item
(integer) 1
> rpush list-key item2
(integer) 2
> rpush list-key item
(integer) 3
> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"
> lindex list-key 1
"item2"
> lpop list-key
"item"
> lrange list-key 0 -1
1) "item2"
2) "item"
```
## 3. SET
<div align="center"> <img src="../pics//cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png" width="400"/> </div><br>
```html
> sadd set-key item
(integer) 1
> sadd set-key item2
(integer) 1
> sadd set-key item3
(integer) 1
> sadd set-key item
(integer) 0
> smembers set-key
1) "item"
2) "item2"
3) "item3"
> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1
> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0
> smembers set-key
1) "item"
2) "item3"
```
## 4. HASH
<div align="center"> <img src="../pics//7bd202a7-93d4-4f3a-a878-af68ae25539a.png" width="400"/> </div><br>
```html
> hset hash-key sub-key1 value1
(integer) 1
> hset hash-key sub-key2 value2
(integer) 1
> hset hash-key sub-key1 value1
(integer) 0
> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"
> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0
> hget hash-key sub-key1
"value1"
> hgetall hash-key
1) "sub-key1"
2) "value1"
```
## 5. ZSET
<div align="center"> <img src="../pics//1202b2d6-9469-4251-bd47-ca6034fb6116.png" width="400"/> </div><br>
```html
> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
```
# 三、键的过期时间
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
过期时间对于清理缓存数据非常有用。
# 四、发布与订阅
发布与订阅实际上是观察者模式,订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。
发布与订阅有一些问题,很少使用它,而是使用替代的解决方案。问题如下:
1. 如果订阅者读取消息的速度很慢,会使得消息不断积压在发布者的输出缓存区中,造成内存占用过多;
2. 如果订阅者在执行订阅的过程中网络出现问题,那么就会丢失断线期间发送的所有消息。
# 五、事务
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
MULTI 和 EXEC 中的操作将会一次性发送给服务器,而不是一条一条发送,这种方式称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
# 六、持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
## 1. 快照持久化
将某个时间点的所有数据都存放到硬盘上。
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。并且如果数据量很大,保存快照的时间也会很长。
## 2. AOF 持久化
AOF 持久化将写命令添加到 AOF 文件Append Only File的末尾。
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。因此将写命令添加到 AOF 文件时,要根据需求来保证何时将添加的数据同步到硬盘上,有以下同步选项:
| 选项 | 同步频率 |
| -- | -- |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
always 选项会严重减低服务器的性能everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
随着服务器写请求的增多AOF 文件会越来越大Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
# 七、复制
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
一个从服务器只能有一个主服务器,并且不支持主主复制。
## 从服务器连接主服务器的过程
1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
## 主从链
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
<div align="center"> <img src="../pics//395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png" width="600"/> </div><br>
# 八、处理故障
要用到持久化文件来恢复服务器的数据。
持久化文件可能因为服务器出错也有错误,因此要先对持久化文件进行验证和修复。对 AOF 文件就行验证和修复很容易,修复操作将第一个出错命令和其后的所有命令都删除;但是只能验证快照文件,无法对快照文件进行修复,因为快照文件进行了压缩,出现在快照文件中间的错误可能会导致整个快照文件的剩余部分无法读取。
当主服务器出现故障时Redis 常用的做法是新开一台服务器作为主服务器,具体步骤如下:假设 A 为主服务器B 为从服务器,当 A 出现故障时,让 B 生成一个快照文件,将快照文件发送给 C并让 C 恢复快照文件的数据。最后,让 B 成为 C 的从服务器。
# 九、分片
Redis 中的分片类似于 MySQL 的分表操作,分片是将数据划分为多个部分的方法,对数据的划分可以基于键包含的 ID、基于键的哈希值或者基于以上两者的某种组合。通过对数据进行分片用户可以将数据存储到多台机器里面也可以从多台机器里面获取数据这种方法在解决某些问题时可以获得线性级别的性能提升。
假设有 4 个 Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
## 1. 客户端分片
客户端使用一致性哈希等算法决定键应当分布到哪个节点。
## 2. 代理分片
将客户端请求发送到代理上,由代理转发请求到正确的节点上。
## 3. 服务器分片
Redis Cluster。
# 十、事件
## 事件类型
### 1. 文件事件
服务器有许多套接字,事件产生时会对这些套接字进行操作,服务器通过监听套接字来处理事件。常见的文件事件有:客户端的连接事件;客户端的命令请求事件;服务器向客户端返回命令结果的事件;
### 2. 时间事件
又分为两类:定时事件是让一段程序在指定的时间之内执行一次;周期性时间是让一段程序每隔指定时间就执行一次。
## 事件的调度与执行
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能监听太久,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
```python
def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
if remaind_ms < 0:
remaind_ms = 0
# 根据 remaind_ms 的值,创建 timeval
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval)
# 处理所有已产生的文件事件
procesFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
```
将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
```python
def main():
# 初始化服务器
init_server()
# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()
```
从事件处理的角度来看,服务器运行流程如下:
<div align="center"> <img src="../pics//73b73189-9e95-47e5-91d0-9378b8462e15.png"/> </div><br>
# 十一、Redis 与 Memcached 的区别
两者都是非关系型内存键值数据库。有以下主要不同:
## 数据类型
Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
## 数据持久化
Redis 支持两种持久化策略RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
## 分布式
Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
Redis Cluster 实现了分布式的支持。
## 内存管理机制
在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
# 十二、Redis 适用场景
## 缓存
适用 Redis 作为缓存,将热点数据放到内存中。
## 消息队列
Redis 的 List 类型是双向链表,很适合用于消息队列。
## 计数器
Redis 这种内存数据库才能支持计数器的频繁读写操作。
## 好友关系
使用 set 类型的交集很容易就可以知道两个用户的共同好友。
# 十三、数据淘汰策略
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
| 策略 | 描述 |
| -- | -- |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| no-envicition | 禁止驱逐数据 |
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
# 十四、一个简单的论坛系统分析
该论坛系统功能如下:
- 可以发布文章;
- 可以对文章进行点赞;
- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示;
## 文章信息
文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
Redis 没有关系型数据库中的表这一概念来将同类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617其中 article 为命名空间ID 为 92617。
<div align="center"> <img src="../pics//7c54de21-e2ff-402e-bc42-4037de1c1592.png" width="400"/> </div><br>
## 点赞功能
当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。
为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。
<div align="center"> <img src="../pics//485fdf34-ccf8-4185-97c6-17374ee719a0.png" width="400"/> </div><br>
## 对文章进行排序
为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)
<div align="center"> <img src="../pics//f7d170a3-e446-4a64-ac2d-cb95028f81a8.png" width="800"/> </div><br>
# 参考资料
- Redis 实战
- Reids 设计与实现
- [REDIS IN ACTION](https://redislabs.com/ebook/foreword/)
- [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)

732
notes/SQL.md Normal file
View File

@ -0,0 +1,732 @@
<!-- GFM-TOC -->
* [一、基础](#一基础)
* [二、创建表](#二创建表)
* [三、修改表](#三修改表)
* [四、插入](#四插入)
* [五、更新](#五更新)
* [六、删除](#六删除)
* [七、查询](#七查询)
* [八、排序](#八排序)
* [九、过滤](#九过滤)
* [十、通配符](#十通配符)
* [十一、计算字段](#十一计算字段)
* [十二、函数](#十二函数)
* [十三、分组](#十三分组)
* [十四、子查询](#十四子查询)
* [十五、连接](#十五连接)
* [十六、组合查询](#十六组合查询)
* [十七、视图](#十七视图)
* [十八、存储过程](#十八存储过程)
* [十九、游标](#十九游标)
* [二十、触发器](#二十触发器)
* [二十一、事务处理](#二十一事务处理)
* [二十二、字符集](#二十二字符集)
* [二十三、权限管理](#二十三权限管理)
<!-- GFM-TOC -->
# 一、基础
模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。
主键的值不允许修改,也不允许复用(不能使用已经删除的主键值赋给新数据行的主键)。
SQLStructured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
SQL 支持以下三种注释:
```sql
# 注释
SELECT *
FROM mytable; -- 注释
/* 注释1
注释2 */
```
# 二、创建表
```sql
CREATE TABLE mytable (
id INT NOT NULL AUTO_INCREMENT,
col1 INT NOT NULL DEFAULT 1,
col2 VARCHAR(45) NULL,
col3 DATE NULL,
PRIMARY KEY (`id`));
```
# 三、修改表
添加列
```sql
ALTER TABLE mytable
ADD col CHAR(20);
```
删除列
```sql
ALTER TABLE mytable
DROP COLUMN col;
```
删除表
```sql
DROP TABLE mytable;
```
# 四、插入
普通插入
```sql
INSERT INTO mytable(col1, col2)
VALUES(val1, val2);
```
插入检索出来的数据
```sql
INSERT INTO mytable1(col1, col2)
SELECT col1, col2
FROM mytable2;
```
将一个表的内容插入到一个新表
```sql
CREATE TABLE newtable AS
SELECT * FROM mytable;
```
# 五、更新
```sql
UPDATE mytable
SET col = val
WHERE id = 1;
```
# 六、删除
```sql
DELETE FROM mytable
WHERE id = 1;
```
**TRUNCATE TABLE** 可以清空表,也就是删除所有行。
使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
# 七、查询
## DISTINCT
相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。
```sql
SELECT DISTINCT col1, col2
FROM mytable;
```
## LIMIT
限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
返回前 5 行:
```sql
SELECT *
FROM mytable
LIMIT 5;
```
```sql
SELECT *
FROM mytable
LIMIT 0, 5;
```
返回第 3 \~ 5 行:
```sql
SELECT *
FROM mytable
LIMIT 2, 3;
```
# 八、排序
- **ASC** :升序(默认)
- **DESC** :降序
可以按多个列进行排序,并且为每个列指定不同的排序方式:
```sql
SELECT *
FROM mytable
ORDER BY col1 DESC, col2 ASC;
```
# 九、过滤
不进行过滤的数据非常大,导致通过网络传输了很多多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
```sql
SELECT *
FROM mytable
WHERE col IS NULL;
```
下表显示了 WHERE 子句可用的操作符
| 操作符 | 说明 |
| ------------ | ------------ |
| = < > | 等于 小于 大于 |
| <> != | 不等于 |
| <= !> | 小于等于 |
| >= !< | 大于等于 |
| BETWEEN | 在两个值之间 |
| IS NULL | 为NULL值 |
应该注意到NULL 与 0 、空字符串都不同。
**AND OR** 用于连接多个过滤条件。优先处理 AND因此当一个过滤表达式涉及到多个 AND 和 OR 时,应当使用 () 来决定优先级。
**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
**NOT** 操作符用于否定一个条件。
# 十、通配符
通配符也是用在过滤语句中,但它只能用于文本字段。
- **%** 匹配 >=0 个任意字符,类似于 \*
- **\_** 匹配 ==1 个任意字符,类似于 \.
- **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。
使用 Like 来进行通配符匹配。
```sql
SELECT *
FROM mytable
WHERE col LIKE '[^AB]%' -- 不以 A 和 B 开头的任意文本
```
不要滥用通配符,通配符位于开头处匹配会非常慢。
# 十一、计算字段
在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。
计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。
```sql
SELECT col1*col2 AS alias
FROM mytable
```
**CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。
```sql
SELECT CONCAT(TRIM(col1), ' (', TRIM(col2), ')')
FROM mytable
```
# 十二、函数
各个 DBMS 的函数都是不相同的,因此不可移植。
## 文本处理
| 函数 | 说明 |
| ------------ | ------------ |
| LEFT() RIGHT() | 左边或者右边的字符 |
| LOWER() UPPER() | 转换为小写或者大写 |
| LTRIM() RTIM() | 去除左边或者右边的空格 |
| LENGTH() | 长度 |
| SUNDEX() | 转换为语音值 |
其中, **SOUNDEX()** 是将一个字符串转换为描述其语音表示的字母数字模式的算法,它是根据发音而不是字母比较。
```sql
SELECT *
FROM mytable
WHERE SOUNDEX(col1) = SOUNDEX('apple')
```
## 日期和时间处理
- 日期格式YYYY-MM-DD
- 时间格式HH:MM:SS
|函 数 | 说 明|
| --- | --- |
| AddDate() | 增加一个日期(天、周等)|
| AddTime() | 增加一个时间(时、分等)|
| CurDate() | 返回当前日期 |
| CurTime() | 返回当前时间 |
| Date() |返回日期时间的日期部分|
| DateDiff() |计算两个日期之差|
| Date_Add() |高度灵活的日期运算函数|
| Date_Format() |返回一个格式化的日期或时间串|
| Day()| 返回一个日期的天数部分|
| DayOfWeek() |对于一个日期,返回对应的星期几|
| Hour() |返回一个时间的小时部分|
| Minute() |返回一个时间的分钟部分|
| Month() |返回一个日期的月份部分|
| Now() |返回当前日期和时间|
| Second() |返回一个时间的秒部分|
| Time() |返回一个日期时间的时间部分|
| Year() |返回一个日期的年份部分|
```sql
mysql> SELECT NOW();
-> '2017-06-28 14:01:52'
```
## 数值处理
| 函数 | 说明 |
| --- | --- |
| SIN() | 正弦 |
| COS() | 余弦 |
| TAN() | 正切 |
| ABS() | 绝对值 |
| SQRT() | 平方根 |
| MOD() | 余数 |
| EXP() | 指数 |
| PI() | 圆周率 |
| RAND() | 随机数 |
## 汇总
|函 数 |说 明|
| --- | --- |
| AVG() | 返回某列的平均值 |
| COUNT() | 返回某列的行数 |
| MAX() | 返回某列的最大值 |
| MIN() | 返回某列的最小值 |
| SUM() |返回某列值之和 |
AVG() 会忽略 NULL 行。
使用 DISTINCT 可以汇总函数值汇总不同的值。
```sql
SELECT AVG(DISTINCT col1) AS avg_col
FROM mytable
```
# 十三、分组
分组就是把具有相同的数据值的行放在同一组中。
可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。
指定的分组字段除了能让数组按该字段进行分组,也可以按该字段进行排序,例如按 col 字段排序并分组数据:
```sql
SELECT col, COUNT(*) AS num
FROM mytable
GROUP BY col;
```
WHERE 过滤行HAVING 过滤分组。行过滤应当先与分组过滤;
```sql
SELECT col, COUNT(*) AS num
FROM mytable
WHERE col > 2
GROUP BY col
HAVING COUNT(*) >= 2;
```
GROUP BY 的排序结果为分组字段,而 ORDER BY 也可以以聚集字段来进行排序。
```sql
SELECT col, COUNT(*) AS num
FROM mytable
GROUP BY col
ORDER BY num;
```
分组规定:
1. GROUP BY 子句出现在 WHERE 子句之后ORDER BY 子句之前;
2. 除了汇总计算语句的字段外SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
3. NULL 的行会单独分为一组;
4. 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
# 十四、子查询
子查询中只能返回一个字段的数据。
可以将子查询的结果作为 WHRER 语句的过滤条件:
```
SELECT *
FROM mytable1
WHERE col1 IN (SELECT col2
FROM mytable2);
```
下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次:
```sql
SELECT cust_name, (SELECT COUNT(*)
FROM Orders
WHERE Orders.cust_id = Customers.cust_id)
AS orders_num
FROM Customers
ORDER BY cust_name;
```
# 十五、连接
连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 Where。
连接可以替换子查询,并且比子查询的效率一般会更快。
可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。
## 内连接
内连接又称等值连接,使用 INNER JOIN 关键字。
```sql
select a, b, c
from A inner join B
on A.key = B.key
```
可以不明确使用 INNER JOIN而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
```sql
select a, b, c
from A, B
where A.key = B.key
```
在没有条件语句的情况下返回笛卡尔积。
## 自连接
自连接可以看成内连接的一种,只是连接的表是自身而已。
一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。
子查询版本
```sql
select name
from employee
where department = (
select department
from employee
where name = "Jim");
```
自连接版本
```sql
select name
from employee as e1, employee as e2
where e1.department = e2.department
and e1.name = "Jim";
```
连接一般比子查询的效率高。
## 自然连接
自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
```sql
select *
from employee natural join department;
```
## 外连接
外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。
检索所有顾客的订单信息,包括还没有订单信息的顾客。
```sql
select Customers.cust_id, Orders.order_num
from Customers left outer join Orders
on Customers.cust_id = Orders.curt_id;
```
如果需要统计顾客的订单数,使用聚集函数。
```sql
select Customers.cust_id,
COUNT(Orders.order_num) as num_ord
from Customers left outer join Orders
on Customers.cust_id = Orders.curt_id
group by Customers.cust_id;
```
# 十六、组合查询
使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果为 M+N 行。
每个查询必须包含相同的列、表达式或者聚集函数。
默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
```sql
SELECT col
FROM mytable
WHERE col = 1
UNION
SELECT col
FROM mytable
WHERE col =2;
```
# 十七、视图
视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。
视图具有如下好处:
1. 简化复杂的 SQL 操作,比如复杂的联结;
2. 只使用实际表的一部分数据;
3. 通过只给用户访问视图的权限,保证数据的安全性;
4. 更改数据格式和表示。
```sql
CREATE VIEW myview AS
SELECT Concat(col1, col2) AS concat_col, col3*col4 AS count_col
FROM mytable
WHERE col5 = val;
```
# 十八、存储过程
存储过程可以看成是对一系列 SQL 操作的批处理;
## 使用存储过程的好处
1. 代码封装,保证了一定的安全性;
2. 代码复用;
3. 由于是预先编译,因此具有很高的性能。
## 创建存储过程
命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
包含 in、out 和 inout 三种参数。
给变量赋值都需要用 select into 语句。
每次只能给一个变量赋值,不支持集合的操作。
```sql
delimiter //
create procedure myprocedure( out ret int )
begin
declare y int;
select sum(col1)
from mytable
into y;
select y*y into ret;
end //
delimiter ;
```
```sql
call myprocedure(@ret);
select @ret;
```
# 十九、游标
在存储过程中使用游标可以对一个结果集进行移动遍历。
游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。
使用游标的四个步骤:
1. 声明游标,这个过程没有实际检索出数据;
2. 打开游标;
3. 取出数据;
4. 关闭游标;
```sql
delimiter //
create procedure myprocedure(out ret int)
begin
declare done boolean default 0;
declare mycursor cursor for
select col1 from mytable;
# 定义了一个continue handler当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
declare continue handler for sqlstate '02000' set done = 1;
open mycursor;
repeat
fetch mycursor into ret;
select ret;
until done end repeat;
close mycursor;
end //
delimiter ;
```
# 二十、触发器
触发器会在某个表执行以下语句时而自动执行DELETE、INSERT、UPDATE
触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化。
INSERT 触发器包含一个名为 NEW 的虚拟表。
```sql
CREATE TRIGGER mytrigger AFTER INSERT ON mytable
FOR EACH ROW SELECT NEW.col;
```
DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改地,而 OLD 是只读的。
可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。
MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储过程。
# 二十一、事务处理
基本术语:
1. 事务transaction指一组 SQL 语句;
2. 回退rollback指撤销指定 SQL 语句的过程;
3. 提交commit指将未存储的 SQL 语句结果写入数据库表;
4. 保留点savepoint指事务处理中设置的临时占位符placeholder你可以对它发布回退与回退整个事务处理不同
不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CRETE 和 DROP 语句。
MySQL 的事务提交默认是隐式提交,也就是每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交autocommit 标记是针对每个连接而不是针对服务器的。
如果没有设置保留点ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
```sql
START TRANSACTION
// ...
SAVEPOINT delete1
// ...
ROLLBACK TO delete1
// ...
COMMIT
```
# 二十二、字符集
基本术语:
1. 字符集为字母和符号的集合;
2. 编码为某个字符集成员的内部表示;
3. 校对字符指定如何比较,主要用于排序和分组。
除了给表指定字符集和校对外,也可以给列指定:
```sql
CREATE TABLE mytable
(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci )
DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;
```
可以在排序、分组时指定校对:
```sql
SELECT *
FROM mytable
ORDER BY col COLLATE latin1_general_ci;
```
# 二十三、权限管理
MySQL 的账户信息保存在 mysql 这个数据库中。
```sql
USE mysql;
SELECT user FROM user;
```
## 创建账户
```sql
CREATE USER myuser IDENTIFIED BY 'mypassword';
```
新创建的账户没有任何权限。
## 修改账户名
```sql
RENAME myuser TO newuser;
```
## 删除账户
```sql
DROP USER myuser;
```
## 查看权限
```sql
SHOW GRANTS FOR myuser;
```
## 授予权限
```sql
GRANT SELECT, INSERT ON mydatabase.* TO myuser;
```
<div align="center"> <img src="../pics//c73aa08e-a987-43c9-92be-adea4a884c25.png"/> </div><br>
账户用 username@host 的形式定义username@% 使用的是默认主机名。
## 删除权限
```sql
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
```
GRANT 和 REVOKE 可在几个层次上控制访问权限:
- 整个服务器,使用 GRANT ALL 和 REVOKE ALL
- 整个数据库,使用 ON database.\*
- 特定的表,使用 ON database.table
- 特定的列;
- 特定的存储过程。
## 更改密码
必须使用 Password() 函数
```sql
SET PASSWROD FOR myuser = Password('newpassword');
```

155
notes/一致性协议.md Normal file
View File

@ -0,0 +1,155 @@
<!-- GFM-TOC -->
* [一、两阶段提交协议](#一两阶段提交协议)
* [二、Paxos 协议](#二paxos-协议)
* [三、Raft 协议](#三raft-协议)
* [四、拜占庭将军问题](#四拜占庭将军问题)
* [五、参考资料](#五参考资料)
<!-- GFM-TOC -->
# 一、两阶段提交协议
Two-phase Commit2PC
可以保证一个事务跨越多个节点时保持 ACID 特性。
两类节点协调者Coordinator和参与者Participants协调者只有一个参与者可以有多个。
## 运行过程
1. 准备阶段:协调者询问参与者事务是否执行成功;
2. 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
<div align="center"> <img src="../pics//07717718-1230-4347-aa18-2041c315e670.jpg"/> </div><br>
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段协接收到协调者发来的通知后,才进行提交或者回滚。
## 存在的问题
- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。
- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。
# 二、Paxos 协议
用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。
主要有三类节点:
1. 提议者Proposer提议一个值
2. 接受者Acceptor对每个提议进行投票
3. 告知者Learner被告知投票的结果不参与投票的过程。
<div align="center"> <img src="../pics//0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg"/> </div><br>
## 执行过程
规定一个提议包含两个字段:[n, v],其中 n 为序号具有唯一性v 为提议值。
下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向每个 Acceptor 发送提议请求。
<div align="center"> <img src="../pics//2bf2fd8f-5ade-48ba-a2b3-74195ac77c4b.png" width="500"/> </div><br>
当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收的提议为 [n1, v1],并且保证以后不会再接受提议值小于 n1 的提议。
如下图Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,并且设置当前接收的提议为 [n=2, v=8],并且保证以后不会再接受提议值小于 2 的提议。其它的 Acceptor 类似。
<div align="center"> <img src="../pics//3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg" width="600"/> </div><br>
如果 Acceptor 接受到一个提议请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2那么就丢弃该提议请求否则发送提议响应该提议响应包含之前已经接收过的提议 [n1, v1],设置当前接收的提议为 [n2, v2],并且保证以后不会再接受提议值小于 n2 的提议。
如下图Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的提议请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2因此就抛弃该提议请求Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的提议请求,因为之前接收的提议为 [n=2, v=8],并且 2 <= 4因此就发送 [n=2, v=8] 的提议响应,设置当前接收的提议为 [n=4, v=5],并且保证以后不会再接受提议值小于 4 的提议。Acceptor Y 类似。
<div align="center"> <img src="../pics//9b829410-86c4-40aa-ba8d-9e8e26c0eeb8.jpg" width="600"/> </div><br>
当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。
如下图Proposer A 接受到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受提议值小于 4 的提议。Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。
<div align="center"> <img src="../pics//2c4556e4-0751-4377-ab08-e7b89d697ca7.png" width="400"/> </div><br>
Acceptor 接收到接受请求时,如果提议号大于等于该 Acceptor 承诺的最小提议号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
<div align="center"> <img src="../pics//8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg" width="500"/> </div><br>
## 约束条件
### 1. 正确性
只有一个提议值会生效。
因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。
### 2. 可终止性
最后总会有一个提议生效。
Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
# 三、Raft 协议
Raft 和 Poxas 类似,但是更容易理解,也更容易实现。
Raft 主要是用来竞选主节点。
## 单个 Candidate 的竞选
有三种节点Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate进入竞选阶段。
① 下图表示一个分布式系统的最初阶段,此时只有 Follower没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
<div align="center"> <img src="../pics//111521118015898.gif"/> </div><br>
② 此时 A 发送投票请求给其它所有节点。
<div align="center"> <img src="../pics//111521118445538.gif"/> </div><br>
③ 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
<div align="center"> <img src="../pics//111521118483039.gif"/> </div><br>
④ 之后 Leader 会周期性地发送心跳包给 FollowerFollower 接收到心跳包,会重新开始计时。
<div align="center"> <img src="../pics//111521118640738.gif"/> </div><br>
## 多个 Candidate 竞选
① 如果有多个 Follower 成为 Candidate并且所获得票数相同那么就需要重新开始投票例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。
<div align="center"> <img src="../pics//111521119203347.gif"/> </div><br>
② 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。
<div align="center"> <img src="../pics//111521119368714.gif"/> </div><br>
## 日志复制
① 来自客户端的修改都会被传入 Leader。注意该修改还未被提交只是写入日志中。
<div align="center"> <img src="../pics//7.gif"/> </div><br>
② Leader 会把修改复制到所有 Follower。
<div align="center"> <img src="../pics//9.gif"/> </div><br>
③ Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
<div align="center"> <img src="../pics//10.gif"/> </div><br>
④ 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
<div align="center"> <img src="../pics//11.gif"/> </div><br>
# 四、拜占庭将军问题
> [拜占庭将军问题深入探讨](http://www.8btc.com/baizhantingjiangjun)
# 五、参考资料
- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details)
- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft)
- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/)

342
notes/代码可读性.md Normal file
View File

@ -0,0 +1,342 @@
<!-- GFM-TOC -->
* [一、可读性的重要性](#一可读性的重要性)
* [二、用名字表达代码含义](#二用名字表达代码含义)
* [三、名字不能带来歧义](#三名字不能带来歧义)
* [四、良好的代码风格](#四良好的代码风格)
* [五、编写注释](#五编写注释)
* [六、如何编写注释](#六如何编写注释)
* [七、提高控制流的可读性](#七提高控制流的可读性)
* [八、拆分长表达式](#八拆分长表达式)
* [九、变量与可读性](#九变量与可读性)
* [十、抽取函数](#十抽取函数)
* [十一、一次只做一件事](#十一一次只做一件事)
* [十二、用自然语言表述代码](#十二用自然语言表述代码)
* [十三、减少代码量](#十三减少代码量)
<!-- GFM-TOC -->
# 一、可读性的重要性
编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。
可读性良好的代码往往会让代码架构更好,因为程序员更愿意去修改这部分代码,而且也更容易修改。
只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。
# 二、用名字表达代码含义
一些比较有表达力的单词:
| 单词 | 可替代单词 |
| --- | --- |
| send | deliver、dispatch、announce、distribute、route |
| find | search、extract、locate、recover |
| start| launch、create、begin、open|
| make | create、set up、build、generate、compose、add、new |
使用 i、j、k 作为循环迭代器的名字过于简单user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高
为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。
# 三、名字不能带来歧义
起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。
用 min、max 表示数量范围;用 first、last 表示访问空间的包含范围begin、end 表示访问空间的排除范围,即 end 不包含尾部。
<div align="center"> <img src="../pics//05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg"/> </div><br>
布尔相关的命名加上 is、can、should、has 等前缀。
# 四、良好的代码风格
适当的空行和缩进。
排列整齐的注释:
```java
int a = 1; // 注释
int b = 11; // 注释
int c = 111; // 注释
```
语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致;
把相关的代码按块组织起来放在一起。
# 五、编写注释
阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
不能因为有注释就随便起个名字,而是争取起个好名字而不写注释。
可以用注释来记录采用当前解决办法的思考过程,从而让读者更容易理解代码。
注释用来提醒一些特殊情况。
用 TODO 等做标记:
| 标记 | 用法 |
|---|---|
|TODO| 待做 |
|FIXME| 待修复 |
|HACH| 粗糙的解决方案 |
|XXX| 危险!这里有重要的问题 |
# 六、如何编写注释
尽量简洁明了:
```java
// The first String is student's name
// The Second Integer is student's score
Map<String, Integer> scoreMap = new HashMap<>();
```
```java
// Student' name -> Student's score
Map<String, Integer> scoreMap = new HashMap<>();
```
添加测试用例来说明:
```java
//...
// Example: add(1, 2), return 3
int add(int x, int y) {
return x + y;
}
```
在很复杂的函数调用中对每个参数标上名字:
```java
int a = 1;
int b = 2;
int num = add(\* x = *\ a, \* y = *\ b);
```
使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。
# 七、提高控制流的可读性
条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确:
```java
if(len < 10)
if(10 > len)
```
if / else 条件语句,逻辑的处理顺序为:① 正逻辑;② 关键逻辑;③ 简单逻辑。
```java
if(a == b) {
// 正逻辑
} else{
// 反逻辑
}
```
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else
do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。
如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。
在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
# 八、拆分长表达式
长表达式的可读性很差,可以引入一些解释变量从而拆分表达式:
```python
if line.split(':')[0].strip() == "root":
...
```
```python
username = line.split(':')[0].strip()
if username == "root":
...
```
使用摩根定理简化一些逻辑表达式:
```java
if(!a && !b) {
...
}
```
```java
if(!(a || b)) {
...
}
```
# 九、变量与可读性
**去除控制流变量** 。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。
```java
boolean done = false;
while(/* condition */ && !done) {
...
if(...) {
done = true;
continue;
}
}
```
```
while(/* condition */) {
...
if(...) {
break;
}
}
```
**减小变量作用域** 。作用域越小,越容易定位到变量所有使用的地方。
JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
```js
submitted = false;
var submit_form = function(form_name) {
if(submitted) {
return;
}
submitted = true;
};
```
```js
var submit_form = (function() {
var submitted = false;
return function(form_name) {
if(submitted) {
return;
}
submitted = true;
}
}()); // () 使得外层匿名函数立即执行
```
JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。
变量定义的位置应当离它使用的位置最近。
**实例解析**
在一个网页中有以下文本输入字段:
```html
<input type = "text" id = "input1" value = "a">
<input type = "text" id = "input2" value = "b">
<input type = "text" id = "input3" value = "">
<input type = "text" id = "input4" value = "d">
```
现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下:
```js
var setFirstEmptyInput = function(new_alue) {
var found = false;
var i = 1;
var elem = document.getElementById('input' + i);
while(elem != null) {
if(elem.value === '') {
found = true;
break;
}
i++;
elem = document.getElementById('input' + i);
}
if(found) elem.value = new_value;
return elem;
}
```
以上实现有以下问题:
- found 可以去除;
- elem 作用域过大;
- 可以用 for 循环代替 while 循环;
```js
var setFirstEmptyInput = function(new_value) {
for(var i = 1; true; i++) {
var elem = document.getElementById('input' + i);
if(elem === null) {
return null;
}
if(elem.value === '') {
elem.value = new_value;
return elem;
}
}
};
```
# 十、抽取函数
工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。
首先应该明确一个函数的高层次目标,然后对于不是直接为了这个目标工作的代码,抽取出来放到独立的函数中。
介绍性的代码:
```java
int findClostElement(int[] arr) {
int clostIdx;
int clostDist = Interger.MAX_VALUE;
for(int i = 0; i < arr.length; i++) {
int x = ...;
int y = ...;
int z = ...;
int value = x * y * z;
int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2));
if(dist < clostDist) {
clostIdx = i;
clostDist = value;
}
}
return clostIdx;
}
```
以上代码中循环部分主要计算距离,这部分不属于代码高层次目标,高层次目标是寻找最小距离的值,因此可以把这部分代替提取到独立的函数中。这样做也带来一个额外的好处有:可以单独进行测试、可以快速找到程序错误并修改。
```java
public int findClostElement(int[] arr) {
int clostIdx;
int clostDist = Interger.MAX_VALUE;
for(int i = 0; i < arr.length; i++) {
int dist = computDist(arr, i);
if(dist < clostDist) {
clostIdx = i;
clostDist = value;
}
}
return clostIdx;
}
```
并不是函数抽取的越多越好,如果抽取过多,在阅读代码的时候可能需要不断跳来跳去。只有在当前函数不需要去了解某一块代码细节而能够表达其内容时,把这块代码抽取成子函数才是好的。
函数抽取也用于减小代码的冗余。
# 十一、一次只做一件事
只做一件事的代码很容易让人知道其要做的事;
基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。
# 十二、用自然语言表述代码
先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。
# 十三、减少代码量
不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。
多用标准库实现。

View File

@ -0,0 +1,14 @@
# Google Java Style Guide
- http://www.hawstein.com/posts/google-java-style.html
- http://google.github.io/styleguide/javaguide.html
# Google C++ Style Guide
- http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/
- http://google.github.io/styleguide/cppguide.html
# Google Python Style Guide
- http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/
- http://google.github.io/styleguide/pyguide.html

206
notes/分布式基础.md Normal file
View File

@ -0,0 +1,206 @@
<!-- GFM-TOC -->
* [一、基本概念](#一基本概念)
* [异常](#异常)
* [超时](#超时)
* [衡量指标](#衡量指标)
* [二、数据分布](#二数据分布)
* [哈希分布](#哈希分布)
* [顺序分布](#顺序分布)
* [三、负载均衡](#三负载均衡)
* [四、复制](#四复制)
* [强同步复制协议](#强同步复制协议)
* [异步复制协议](#异步复制协议)
* [五、CAP](#五cap)
* [六、BASE](#六base)
* [基本可用](#基本可用)
* [软状态](#软状态)
* [最终一致性](#最终一致性)
* [七、容错](#七容错)
* [故障检测](#故障检测)
* [故障恢复](#故障恢复)
* [八、CDN 架构](#八cdn-架构)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、基本概念
## 异常
### 1. 服务器宕机
内存错误、服务器停电等都会导致服务器宕机,此时节点无法正常工作,称为不可用。
服务器宕机会导致节点失去所有内存信息,因此需要将内存信息保存到持久化介质上。
### 2. 网络异常
有一种特殊的网络异常称为 **网络分区** ,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
### 3. 磁盘故障
磁盘故障是一种发生概率很高的异常。
使用冗余机制,将数据存储到多台服务器。
## 超时
在分布式系统中,一个请求除了成功和失败两种状态,还存在着超时状态。
<div align="center"> <img src="../pics//b0e8ef47-2f23-4379-8c64-10d5cb44d438.jpg"/> </div><br>
可以将服务器的操作设计为具有 **幂等性** ,即执行多次的结果与执行一次的结果相同。如果使用这种方式,当出现超时的时候,可以不断地重新请求直到成功。
## 衡量指标
### 1. 性能
常见的性能指标有:吞吐量、响应时间。
其中,吞吐量指系统在某一段时间可以处理的请求总数,通常为每秒的读操作数或者写操作数;响应时间指从某个请求发出到接收到返回结果消耗的时间。
这两个指标往往是矛盾的,追求高吞吐的系统,往往很难做到低响应时间,解释如下:
- 在无并发的系统中,吞吐量为响应时间的倒数,例如响应时间为 10 ms那么吞吐量为 100 req/s因此高吞吐也就意味着低响应时间。
- 但是在并发的系统中,由于一个请求在调用 I/O 资源的时候,需要进行等待。服务器端一般使用的是异步等待方式,即等待的请求被阻塞之后不需要一直占用 CPU 资源。这种方式能大大提高 CPU 资源的利用率,例如上面的例子中,单个请求在无并发的系统中响应时间为 10 ms如果在并发的系统中那么吞吐量将大于 100 req/s。因此为了追求高吞吐量通常会提高并发程度。但是并发程度的增加会导致请求的平均响应时间也增加因为请求不能马上被处理需要和其它请求一起进行并发处理响应时间自然就会增高。
### 2. 可用性
可用性指系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量4 个 9 的可用性表示系统 99.99% 的时间是可用的。
### 3. 一致性
可以从两个角度理解一致性:从客户端的角度,读写操作是否满足某种特性;从服务器的角度,多个数据副本之间是否一致。
有以下三种一致性模型:
1. 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值;
2. 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值;
3. 最终一致性:新数据写入之后,只能保证过一了一个时间窗口才能读取到最新值;
### 4. 可扩展性
指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线程增加。
# 二、数据分布
分布式系统的数据分布在多个节点中,常用的数据分布方式有哈希分布和顺序分布。
## 哈希分布
哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key则将该数据分配的节点序号为hash(key)%N。
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
#### 一致性哈希
Distributed Hash TableDHT对于哈希空间 0\~2<sup>n</sup>,将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
<div align="center"> <img src="../pics//d2d34239-e7c1-482b-b33e-3170c5943556.jpg"/> </div><br>
一致性哈希的优点是在加入或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。
<div align="center"> <img src="../pics//91ef04e4-923a-4277-99c0-6be4ce81e5ac.jpg"/> </div><br>
## 顺序分布
哈希分布式破坏了数据的有序性,顺序分布则不会。
顺序分布的数据划分为多个连续的部分按一定策略分布到不同节点上。例如下图中User 表的主键范围为 1 \~ 7000使用顺序分布可以将其划分成多个子表对应的主键范围为 1 \~ 10001001 \~ 2000...6001 \~ 7000。
其中 Meta 表是为了支持更大的集群规模,它将原来的一层索引结分成两层,使用 Meta 表来维护 User 子表所在的节点,从而减轻 Root 节点的负担。
<div align="center"> <img src="../pics//8f64e9c5-7682-4feb-9312-dea09514e160.jpg"/> </div><br>
# 三、负载均衡
衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。分布式系统应当能够自动负载均衡当某个节点的负载较高将它的部分数据迁移到其它节点。
每个集群都有一个总控节点其它节点为工作节点由总控节点根据全局负载信息进行整体调度工作节点定时发送心跳包Heartbeat将节点负载相关的信息发送给总控节点。
一个新上线的工作节点,由于其负载较低,如果不加控制,总控节点会将大量数据同时迁移到该节点上,造成该节点一段时间内无法工作。因此负载均衡操作需要平滑进行,新加入的节点需要较长的一段时间来达到比较均衡的状态。
# 四、复制
复制是保证分布式系统高可用的基础,让一个数据存储多个副本,当某个副本所在的节点出现故障时,能够自动切换到其它副本上,从而实现故障恢复。
多个副本通常有一个为主副本,其它为备副本。主副本用来处理写请求,备副本主要用来处理读请求,实现读写分离。主副本将同步操作日志发送给备副本,备副本通过回放操作日志获取最新修改。
<div align="center"> <img src="../pics//44e4a7ab-215c-41a1-8e34-f55f6c09e517.jpg"/> </div><br>
主备副本之间有两种复制协议,一种是强同步复制协议,一种是异步复制协议。
## 强同步复制协议
要求主副本将同步操作日志发给备副本之后进行等待,要求至少一个备副本返回成功后,才开始修改主副本,修改完成之后通知客户端操作成功。
优点:至少有一个备副本拥有完整的数据,出现故障时可以安全地切换到该备副本,因此一致性好。
缺点:可用性差,因为主副本需要等待,那么整个分布式系统的可用时间就会降低。
## 异步复制协议
主副本将同步操作日志发给备副本之后不需要进行等待,直接修改主副本并通知客户端操作成功。
优点:可用性好。
缺点:一致性差。
# 五、CAP
分布式系统不可能同时满足一致性CConsistency、可用性AAvailability和分区容忍性PPartition tolerance最多只能同时满足其中两项。这三个概念上文中已经提到。
在设计分布式系统时需要根据实际需求弱化某一要求。因此就有了下图中的三种设计CA、CP 和 AP。
<div align="center"> <img src="../pics//f50bc364-fdc2-4a46-9b8f-f8f5b6add3b8.jpg" width="300"/> </div><br>
需要注意的是,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此实际上设计分布式系统需要在一致性和可用性之间做权衡。
# 六、BASE
BASE 是 Basically Available基本可用、Soft state软状态和 Eventually consistent最终一致性三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
## 基本可用
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
例如,电商在做促销时,服务层可能只提供降级服务,部分用户可能会被引导到降级页面上。
## 软状态
指允许系统存在中间状态,而该中间状态不会影响系统整体可用性,即不同节点的数据副本之间进行同步的过程允许存在延时。
## 最终一致性
指所有的数据副本,在经过一段时间的同步之后,最终都能够达到一致的状态。
强一致性需要保证数据副本实时一致,而最终一致性只需要保证过一段时间是一致的。
ACID 是传统数据库系统常用的设计理论追求强一致性模型。BASE 常用于大型分布式系统,只需要保证最终一致性。在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
# 七、容错
分布式系统故障发生的概率很大,为了实现高可用以及减少人工运维成本,需要实现自动化容错。
## 故障检测
通过 **租约机制** 来对故障进行检测。假设节点 A 为主控节点,节点 A 向节点 B 发送租约,节点 B 在租约规定的期限内才能提供服务。期限快到达时,节点 B 需要向 A 重新申请租约。
如果过期,那么 B 不再提供服务,并且 A 也能知道 B 此时可能发生故障并已经停止服务。可以看到通过这种机制A 和 B 都能对 B 发生故障这一事实达成一致。
## 故障恢复
当某个节点故障时,就将它上面的服务迁移到其它节点。
# 八、CDN 架构
CND 通过将内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。不仅可以减轻服务器的负担,也可以提高用户的访问速度。
从下图可以看出DNS 在对域名解析时不再向用户返回源服务器的 IP 地址,而是返回边缘节点的 IP 地址,所以用户最终访问的是边缘节点。边缘节点会先从源服务器中获取用户所需的数据,如果请求成功,边缘节点会将页面缓存下来,下次用户访问时可以直接读取。
<div align="center"> <img src="../pics//dbd60b1f-b700-4da6-a993-62578e892333.jpg"/> </div><br>
# 参考资料
- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.

View File

@ -0,0 +1,316 @@
<!-- GFM-TOC -->
* [一、谈谈业务中使用分布式的场景](#一谈谈业务中使用分布式的场景)
* [二、分布式事务](#二分布式事务)
* [产生原因](#产生原因)
* [应用场景](#应用场景)
* [解决方案](#解决方案)
* [三、负载均衡的算法与实现](#三负载均衡的算法与实现)
* [算法](#算法)
* [实现](#实现)
* [四、分布式锁](#四分布式锁)
* [使用场景](#使用场景)
* [实现方式](#实现方式)
* [五、分布式 Session](#五分布式-session)
* [1. Sticky Sessions](#1-sticky-sessions)
* [2. Session Replication](#2-session-replication)
* [3. Persistent DataStore](#3-persistent-datastore)
* [4. In-Memory DataStore](#4-in-memory-datastore)
* [六、分库与分表带来的分布式困境与应对之策](#六分库与分表带来的分布式困境与应对之策)
* [事务问题](#事务问题)
* [查询问题](#查询问题)
* [ID 唯一性](#id-唯一性)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、谈谈业务中使用分布式的场景
分布式主要是为了提供可扩展性以及高可用性,业务中使用分布式的场景主要有分布式存储以及分布式计算。
分布式存储中可以将数据分片到多个节点上,不仅可以提高性能(可扩展性),同时也可以使用多个节点对同一份数据进行备份(高可用性)。
至于分布式计算就是将一个大的计算任务分解成小任务分配到多台节点上去执行再汇总每个小任务的执行结果得到最终结果。MapReduce 是分布式计算的最好例子。
# 二、分布式事务
指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。
## 产生原因
- 数据库分库分表;
- SOA 架构,比如一个电商网站将订单业务和库存业务分离出来放到不同的节点上。
## 应用场景
- 下单:减少库存、更新订单状态。库存和订单不在同一个数据库,因此涉及分布式事务。
- 支付:买家账户扣款、卖家账户入账。买家和卖家账户信息不在同一个数据库,因此涉及分布式事务。
## 解决方案
### 1. 两阶段提交协议
[两阶段提交](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE.md#%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%E5%8D%8F%E8%AE%AE)
两阶段提交协议可以很好得解决分布式事务问题,它可以使用 XA 来实现XA 它包含两个部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口;而事务管理器作为全局的协调者,负责各个本地资源的提交和回滚。
### 2. 消息中间件
消息中间件也可称作消息系统 (MQ),它本质上是一个暂存转发消息的一个中间件。在分布式应用当中,我们可以把一个业务操作转换成一个消息,比如支付宝的余额转入余额宝操作,支付宝系统执行减少余额操作之后向消息系统发送一个消息,余额宝系统订阅这条消息然后进行增加余额宝操作。
**(一)消息处理模型**
<font size=3> **点对点** </font></br>
<div align="center"> <img src="../pics//96b63e13-e2d8-4ddb-9aa1-a38959ca96e5.jpg" width="600"/> </div><br>
<font size=3> **发布/订阅** </font></br>
<div align="center"> <img src="../pics//654acfed-a6a5-4fc7-8f40-3fdcae57bae8.jpg" width="700"/> </div><br>
**(二)消息的可靠性**
消息的发送端的可靠性:发送端完成操作后一定能将消息成功发送到消息系统。
消息的接收端的可靠性:接收端仅且能够从消息中间件成功消费一次消息。
<font size=3> **发送端的可靠性** </font></br>
在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。
<font size=3> **接收端的可靠性** </font></br>
保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。
保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
# 三、负载均衡的算法与实现
## 算法
### 1. 轮询Round Robin
轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1(2, 4, 6) 的请求会被发送到服务器 2。
<div align="center"> <img src="../pics//2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg"/> </div><br>
该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担多大的负载。下图中,服务器 2 的性能比服务器 1 差,那么服务器 2 可能无法承担多大的负载。
<div align="center"> <img src="../pics//f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg"/> </div><br>
### 2. 加权轮询Weighted Round Robbin
加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5服务器 2 被赋予的权值为 1那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1(6) 请求会被发送到服务器 2。
<div align="center"> <img src="../pics//211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg"/> </div><br>
### 3. 最少连接least Connections
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数多大,而另一台服务器的连接多小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1(2, 4, 6) 请求被发送到服务器 2只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担多大的负载。
<div align="center"> <img src="../pics//3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg"/> </div><br>
最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
<div align="center"> <img src="../pics//1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg"/> </div><br>
### 4. 加权最小连接Weighted Least Connection
在最小连接的基础上,根据服务器的性能为每台服务器分配权重,根据权重计算出每台服务器能处理的连接数。
<div align="center"> <img src="../pics//44edefb7-4b58-4519-b8ee-4aca01697b78.jpg"/> </div><br>
### 5. 随机算法Random
把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。
<div align="center"> <img src="../pics//0ee0f61b-c782-441e-bf34-665650198ae0.jpg"/> </div><br>
## 实现
### 1. HTTP 重定向
HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。
缺点:
- 用户访问的延迟会增加;
- 如果负载均衡器宕机,就无法访问该站点。
<div align="center"> <img src="../pics//10bdf7bf-0daa-4a26-b927-f142b3f8e72b.png"/> </div><br>
### 2. DNS 重定向
使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。
缺点:
- DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。
<div align="center"> <img src="../pics//f8b16d1e-7363-4544-94d6-4939fdf849dc.png"/> </div><br>
### 3. 修改 MAC 地址
使用 LVSLinux Virtual Server这种链路层负载均衡器根据负载情况修改请求的 MAC 地址。
<div align="center"> <img src="../pics//f0e35b7a-2948-488a-a5a9-97d3f6b5e2d7.png"/> </div><br>
### 4. 修改 IP 地址
在网络层修改请求的目的 IP 地址。
<div align="center"> <img src="../pics//265a355d-aead-48aa-b455-f33b62fe729f.png"/> </div><br>
### 5. 代理自动配置
正向代理与反向代理的区别:
- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
- 反向代理:发生在服务器端,用户不知道代理的存在。
PAC 服务器是用来判断一个请求是否要经过代理。
<div align="center"> <img src="../pics//52e1af6f-3a7a-4bee-aa8f-fcb5dacebe40.jpg"/> </div><br>
# 四、分布式锁
Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchronized 和 JDK 提供的 Lock当你的应用是单机或者说单进程应用时可以使用 synchronized 或 Lock 来实现锁。当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。
## 使用场景
在服务器端使用分布式部署的情况下,一个服务可能分布在不同的节点撒花姑娘,比如订单服务分布式在节点 A 和节点 B 上。如果多个客户端同时对一个服务进行请求时,就需要使用分布式锁。例如当 APP 端的请求路由到了节点 AWEB 端被路由到了 B 服务,这时对共享资源进行使用时需要使用分布式锁。
## 实现方式
### 1. 数据库分布式锁
**(一)基于 MySQL 锁表**
该实现方式完全依靠数据库唯一索引来实现。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。这种方式存在以下几个问题:
1. 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
2. 只能是非阻塞锁,插入失败直接就报错了,无法重试。
3. 不可重入,同一线程在没有释放锁之前无法再获得锁。
**(二)采用乐观锁增加版本号**
根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
### 2. Redis 分布式锁
**(一)基于 SETNX、EXPIRE**
使用 SETNXset if not exist命令插入一个键值对时如果 Key 已经存在,那么会返回 False否则插入成功并返回 True。因此客户端在尝试获得锁时先使用 SETNX 向 Redis 中插入一个记录,如果返回 True 表示获得锁,返回 False 表示已经有客户端占用锁。
EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
**RedLock 算法**
RedLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时还可用。
1. 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
2. 计算获取锁消耗的时间只有当这个时间小于锁的过期时间并且从大多数N/2+1实例上获取了锁那么就认为锁获取成功了。
3. 如果锁获取失败,会到每个实例上释放锁。
### 3. Zookeeper 分布式锁
Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
**(一)抽象模型**
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。
<div align="center"> <img src="../pics//31d99967-1171-448e-8531-bccf5c14cffe.jpg" width="400"/> </div><br>
**(二)节点类型**
- 永久节点:不会因为会话结束或者超时而消失;
- 临时节点:如果会话结束或者超时就会消失;
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000它的下一个有序节点则为 /lock/node-0000000001依次类推。
**(三)监听器**
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
**(四)分布式锁实现**
1. 创建一个锁目录 /lock。
1. 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000第二个为 /lock/lock-0000000001以此类推。
2. 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
3. 执行业务代码,完成后,删除对应的子节点。
**(五)会话超时**
如果一个已经获得锁的会话超时了因为创建的是临时节点因此该会话对应的临时节点会被删除其它会话就可以获得锁了。可以看到Zookeeper 分布式锁不会出现数据库分布式锁的死锁问题。
**(六)羊群效应**
在步骤二,一个节点未获得锁,需要监听监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知,而我们只希望它的下一个子节点收到通知。
# 五、分布式 Session
在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有保存用户的 Session就可能导致用户需要重新进行登录等操作。
<div align="center"> <img src="../pics//cookiedata.png"/> </div><br>
## 1. Sticky Sessions
需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。
缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。
<div align="center"> <img src="../pics//MultiNode-StickySessions.jpg"/> </div><br>
## 2. Session Replication
在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。
缺点:需要更好的服务器硬件条件;需要对服务器进行配置。
<div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br>
## 3. Persistent DataStore
将 Session 信息持久化到一个数据库中。
缺点:有可能需要去实现存取 Session 的代码。
<div align="center"> <img src="../pics//MultiNode-SpringSession.jpg"/> </div><br>
## 4. In-Memory DataStore
可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。
# 六、分库与分表带来的分布式困境与应对之策
<div align="center"> <img src="../pics//f3d3e072-e947-43e9-b999-22385fd569a0.jpg"/> </div><br>
## 事务问题
使用分布式事务。
## 查询问题
使用汇总表。
## ID 唯一性
- 使用全局唯一 IDGUID。
- 为每个分片指定一个 ID 范围。
- 分布式 ID 生成器 (如 Twitter 的 [Snowflake](https://twitter.github.io/twitter-server/) 算法)。
# 参考资料
- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms)
- [负载均衡算法及手段](https://segmentfault.com/a/1190000004492447)
- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#)
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)
- [Apache Wicket User Guide - Reference Documentation](# Apache Wicket User Guide - Reference Documentation)
- [集群/分布式环境下 5 种 Session 处理策略](http://blog.csdn.net/u010028869/article/details/50773174?ref=myread)
- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023)
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
- [关于分布式事务](http://blog.csdn.net/suifeng3051/article/details/52691210)
- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
- [服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策")
- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases)

2581
notes/剑指 offer 题解.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,611 @@
<!-- GFM-TOC -->
* [一、事务](#一事务)
* [概念](#概念)
* [四大特性](#四大特性)
* [二、并发一致性问题](#二并发一致性问题)
* [问题](#问题)
* [解决方法](#解决方法)
* [三、封锁](#三封锁)
* [封锁粒度](#封锁粒度)
* [封锁类型](#封锁类型)
* [封锁协议](#封锁协议)
* [四、隔离级别](#四隔离级别)
* [五、多版本并发控制](#五多版本并发控制)
* [版本号](#版本号)
* [Undo 日志](#undo-日志)
* [实现过程](#实现过程)
* [快照读与当前读](#快照读与当前读)
* [六、Next-Key Locks](#六next-key-locks)
* [Record Locks](#record-locks)
* [Grap Locks](#grap-locks)
* [Next-Key Locks](#next-key-locks)
* [七、关系数据库设计理论](#七关系数据库设计理论)
* [函数依赖](#函数依赖)
* [异常](#异常)
* [范式](#范式)
* [八、数据库系统概述](#八数据库系统概述)
* [基本术语](#基本术语)
* [数据库的三层模式和两层映像](#数据库的三层模式和两层映像)
* [九、关系数据库建模](#九关系数据库建模)
* [ER 图](#er-图)
* [十、约束](#十约束)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、事务
## 概念
<div align="center"> <img src="../pics//4f4deaf4-8487-4de2-9d62-5ad017ee9589.png"/> </div><br>
事务指的是满足 ACID 特性的一系列操作。在数据库中,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
## 四大特性
<div align="center"> <img src="../pics//fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg" width="500"/> </div><br>
### 1. 原子性Atomicity
事务被视为不可分割的最小单元,要么全部提交成功,要么全部失败回滚。
### 2. 一致性Consistency
事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。
### 3. 隔离性Isolation
一个事务所做的修改在最终提交以前,对其它事务是不可见的。
### 4. 持久性Durability
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。可以通过数据库备份和恢复来保证持久性。
# 二、并发一致性问题
在并发环境下,一个事务如果受到另一个事务的影响,那么事务操作就无法满足一致性条件。
## 问题
### 1. 丢失修改
T<sub>1</sub> 和 T<sub>2</sub> 两个事务都对一个数据进行修改T<sub>1</sub> 先修改T<sub>2</sub> 随后修改T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
### 2. 读脏数据
T<sub>1</sub> 修改一个数据T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改,那么 T<sub>2</sub> 读取的数据是脏数据。
<div align="center"> <img src="../pics//d1ab24fa-1a25-4804-aa91-513df55cbaa6.jpg" width="800"/> </div><br>
### 3. 不可重复读
T<sub>2</sub> 读取一个数据T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和和第一次读取的结果不同。
<div align="center"> <img src="../pics//d0175e0c-859e-4991-b263-8378e52f7ee5.jpg" width="800"/> </div><br>
### 4. 幻影读
T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插入新的数据T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
<div align="center"> <img src="../pics//d589eca6-c7cf-49c5-ac96-8e4ca0cccadd.jpg" width="800"/> </div><br>
## 解决方法
产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。
在没有并发的情况下,事务以串行的方式执行,互不干扰,因此可以保证隔离性。在并发的情况下,如果能通过并发控制,让事务的执行结果和某一个串行执行的结果相同,就认为事务的执行结果满足隔离性要求,也就是说是正确的。把这种事务执行方式称为 **可串行化调度**
**并发控制可以通过封锁来实现,但是封锁操作都要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。**
# 三、封锁
## 封锁粒度
<div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br>
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。需要在锁开销以及数据安全性之间做一个权衡。
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
## 封锁类型
### 1. 排它锁与共享锁
- 排它锁Exclusive简写为 X 锁,又称写锁。
- 共享锁Shared简写为 S 锁,又称读锁。
有以下两个规定:
1. 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
2. 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
锁的兼容关系如下:
| - | X | S |
| :--: | :--: | :--: |
|X|NO|No|
|S|No|Yes|
### 2. 意向锁
意向锁Intention Locks可以支持多粒度封锁。它本身是一个表锁通过在原来的 X/S 锁之上引入了 IX/IS用来表示一个事务想要在某个数据行上加 X 锁或 S 锁。
有以下两个规定:
1. 一个事务在获得某个数据行对象的 S 锁之前,必须先获得 IS 锁或者更强的锁;
2. 一个事务在获得某个数据行对象的 X 锁之前,必须先获得 IX 锁。
各种锁的兼容关系如下:
| - | X | IX | S | IS |
| :--: | :--: | :--: | :--: | :--: |
|X |No |No |No | No|
|IX |No |YES|No | Yes|
|S |No |No |Yes| Yes|
|IS |No |Yes|Yes| Yes|
## 封锁协议
### 1. 三级封锁协议
**一级封锁协议**
事务 T 要修改数据 A 时必须加 X 锁,直到事务结束才释放锁。
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么一个事务的修改就不会被覆盖。
| T<sub>1</sub> | T<sub>1</sub> |
| :--: | :--: |
| lock-x(A) | |
| read A=20 | |
| | lock-x(A) |
| | wait |
| write A=19 | |
| commit | |
| unlock-x(A) | |
| | obtain |
| | read A=19 |
| | write A=21 |
| | commit |
| | unlock-x(A)|
**二级封锁协议**
在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
| T<sub>1</sub> | T<sub>1</sub> |
| :--: | :--: |
| lock-x(A) | |
| read A=20 | |
| write A=19 | |
| | lock-s(A) |
| | wait |
| rollback | |
| A=20 | |
| unlock-x(A) | |
| | obtain |
| | read A=20 |
| | commit |
| | unlock-s(A)|
**三级封锁协议**
在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
| T<sub>1</sub> | T<sub>1</sub> |
| :--: | :--: |
| lock-s(A) | |
| read A=20 | |
| |lock-x(A) |
| | wait |
| read A=20| |
| commit | |
| unlock-s(A) | |
| | obtain |
| | read A=20 |
| | write A=19|
| | commit |
| | unlock-X(A)|
### 2. 两段锁协议
加锁和解锁分为两个阶段进行,事务 T 对数据 A 进行读或者写操作之前,必须先获得对 A 的封锁并且在释放一个封锁之前T 不能再获得任何的其它锁。
事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。
```html
lock-x(A)...lock-s(B)...lock-s(c)...unlock(A)...unlock(C)...unlock(B)
```
但不是必要条件,例如以下操作不满足两段锁协议,但是它还是可串行化调度。
```html
lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(c)...unlock(C)...
```
# 四、隔离级别
<font size=4> **1. 未提交读READ UNCOMMITTED** </font> </br>
事务中的修改,即使没有提交,对其它事务也是可见的。
<font size=4> **2. 提交读READ COMMITTED** </font> </br>
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
<font size=4> **3. 可重复读REPEATABLE READ** </font> </br>
保证在同一个事务中多次读取同样数据的结果是一样的。
<font size=4> **4. 可串行化SERIALIXABLE** </font> </br>
强制事务串行执行。
<font size=4> **四个隔离级别的对比** </font> </br>
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
| :---: | :---: | :---:| :---: |
| 未提交读 | YES | YES | YES |
| 提交读 | NO | YES | YES |
| 可重复读 | NO | NO | YES |
| 可串行化 | NO | NO | NO |
# 五、多版本并发控制
Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁单纯使用 MVCC 无法实现。
## 版本号
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 事务版本号:事务开始时的系统版本号。
InooDB 的 MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
- 创建版本号:指示创建一个数据行的快照时的系统版本号;
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
## Undo 日志
InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。
<div align="center"> <img src="../pics//e41405a8-7c05-4f70-8092-e961e28d3112.jpg"/> </div><br>
## 实现过程
以下过程针对可重复读隔离级别。
### 1. SELECT
该操作必须保证多个事务读取到同一个数据行的快照,这个快照是最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
当开始新一个事务时,该事务的版本号肯定会大于所有数据行快照的创建版本号,理解这一点很关键。
把没对一个数据行做修改的事务称为 T<sub>1</sub>T<sub>1</sub> 所要读取的数据行快照的创建版本号必须小于当前事务的版本号,因为如果大于或者等于当前事务的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
除了上面的要求T<sub>1</sub> 所要读取的数据行快照的删除版本号必须小于当前事务版本号,因为如果大于或者等于当前事务版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
### 2. INSERT
将系统版本号作为数据行快照的创建版本号。
### 3. DELETE
将系统版本号作为数据行快照的删除版本号。
### 4. UPDATE
将系统版本号作为更新后的数据行快照的创建版本号,同时将系统版本号作为作为更新前的数据行快照的删除版本号。可以理解为新执行 DELETE 后执行 INSERT。
## 快照读与当前读
快照读读指的是读取快照中的数据,而当前读指的是读取最新的数据。
当前读:
```sql
select * from table ....;
```
快照读:
```sql
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;
```
引入当前读的目的主要是为了免去加锁操作带来的性能开销,但是快照读需要加锁。
# 六、Next-Key Locks
Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种实现。MVCC 不能解决幻读的问题Next-Key Locks 就是为了解决这个问题而存在的。在可重复读隔离级别下MVCC + Next-Key Locks就可以防止幻读的出现。
## Record Locks
锁定的对象时索引而不是数据。如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Lock 依然可以使用。
## Grap Locks
锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c1 中插入 15。
```sql
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
```
## Next-Key Locks
它是 Record Locks 和 Gap Locks 的结合。在 user 中有以下记录:
```sql
| id | last_name | first_name | age |
|------|-------------|--------------|-------|
| 4 | stark | tony | 21 |
| 1 | tom | hiddleston | 30 |
| 3 | morgan | freeman | 40 |
| 5 | jeff | dean | 50 |
| 2 | donald | trump | 80 |
+------|-------------|--------------|-------+
```
那么就需要锁定以下范围:
```sql
(-∞, 21]
(21, 30]
(30, 40]
(40, 50]
(50, 80]
(80, ∞)
```
# 七、关系数据库设计理论
## 函数依赖
记 A->B 表示 A 函数决定 B也可以说 B 函数依赖于 A。
如果 {A1A2... An} 是关系的一个或多个属性的集合,该集合决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
对于 W->A如果能找到 W 的真子集 W',使得 W'-> A那么 W->A 就是部分函数依赖,否则就是完全函数依赖;
## 异常
以下的学生课程关系的函数依赖为 Sno, Cname -> Sname, Sdept, Mname, Grade键码为 {Sno, Cname}。也就是说,确定学生和课程之后,就能确定其它信息。
| Sno | Sname | Sdept | Mname | Cname | Grade |
| :---: | :---: | :---: | :---: | :---: |:---:|
| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 |
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 |
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 |
不符合范式的关系,会产生很多异常,主要有以下四种异常:
1. 冗余数据,例如学生-2 出现了两次。
2. 修改异常,修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
3. 删除异常,删除一个信息,那么也会丢失其它信息。例如如果删除了课程-1需要删除第二行和第三行那么学生-1 的信息就会丢失。
4. 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
## 范式
范式理论是为了解决以上提到四种异常。高级别范式的依赖于低级别的范式。
<div align="center"> <img src="../pics//c2d343f7-604c-4856-9a3c-c71d6f67fecc.png" width="300"/> </div><br>
### 1. 第一范式 (1NF)
属性不可分;
### 2. 第二范式 (2NF)
每个非主属性完全函数依赖于键码。
可以通过分解来满足。
<font size=4> **分解前** </font><br>
| Sno | Sname | Sdept | Mname | Cname | Grade |
| :---: | :---: | :---: | :---: | :---: |:---:|
| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 |
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 |
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 |
以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖:
- Sno, Cname -> Sname, Sdept, Mname
- Son -> Sname, Sdept
- Sdept -> Mname
- Sno -> Manme
- Sno, Cname-> Grade
Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。
Sname, Sdept 和 Manme 都函数依赖于 Sno而部分依赖于键码。当一个学生选修了多门课时这些数据就会出现多次造成大量冗余数据。
<font size=4> **分解后** </font><br>
关系-1
| Sno | Sname | Sdept | Mname |
| :---: | :---: | :---: | :---: |
| 1 | 学生-1 | 学院-1 | 院长-1 |
| 2 | 学生-2 | 学院-2 | 院长-2 |
有以下函数依赖:
- Sno -> Sname, Sdept, Mname
- Sdept -> Mname
关系-2
| Sno | Cname | Grade |
| :---: | :---: |:---:|
| 1 | 课程-1 | 90 |
| 2 | 课程-2 | 80 |
| 2 | 课程-1 | 100 |
有以下函数依赖:
- Sno, Cname -> Grade
### 3. 第三范式 (3NF)
非主属性不传递依赖于键码。
上面的关系-1 中存在以下传递依赖Sno -> Sdept -> Mname可以进行以下分解
关系-11
| Sno | Sname | Sdept |
| :---: | :---: | :---: |
| 1 | 学生-1 | 学院-1 |
| 2 | 学生-2 | 学院-2 |
关系-12
| Sdept | Mname |
| :---: | :---: |
| 学院-1 | 院长-1 |
| 学院-2 | 院长-2 |
### 4. BC 范式BCNF
所有属性不传递依赖于键码。
关系 STC(Sname, Tname, Cname, Grade) 的四个属性分别为学生姓名、教师姓名、课程名和成绩,它的键码为 (Sname, Cname, Tname),有以下函数依赖:
- Sname, Cname -> Tname
- Sname, Cname -> Grade
- Sname, Tname -> Cname
- Sname, Tname -> Grade
- Tname -> Cname
存在着以下函数传递依赖:
- Sname -> Tname -> Cname
可以分解成 SC(Sname, Cname, Grade) 和 ST(Sname, Tname),对于 ST属性之间是多对多关系无函数依赖。
# 八、数据库系统概述
## 基本术语
### 1. 数据模型
由数据结构、数据操作和完整性三个要素组成。
### 2. 数据库系统
数据库系统包含所有与数据库相关的内容,包括数据库、数据库管理系统、应用程序以及数据库管理员和用户,还包括相关的硬件和软件。
## 数据库的三层模式和两层映像
- 外模式:局部逻辑结构
- 模式:全局逻辑结构
- 内模式:物理结构
<div align="center"> <img src="../pics//20150928140509757.png" width="600"/> </div><br>
### 1. 外模式
又称用户模式,是用户和数据库系统的接口,特定的用户只能访问数据库系统提供给他的外模式中的数据。例如不同的用户创建了不同数据库,那么一个用户只能访问他有权限访问的数据库。
一个数据库可以有多个外模式,一个用户只能有一个外模式,但是一个外模式可以给多个用户使用。
### 2. 模式
可以分为概念模式和逻辑模式,概念模式可以用概念-关系来描述;逻辑模式使用特定的数据模式(比如关系模型)来描述数据的逻辑结构,这种逻辑结构包括数据的组成、数据项的名称、类型、取值范围。不仅如此,逻辑模式还要描述数据之间的关系、数据的完整性与安全性要求。
### 3. 内模式
又称为存储模式,描述记录的存储方式,例如索引的组织方式、数据是否压缩以及是否加密等等。
### 4. 外模式/模式映像
把外模式的局部逻辑结构和模式的全局逻辑结构联系起来。该映像可以保证数据和应用程序的逻辑独立性。
### 5. 模式/内模式映像
把模式的全局逻辑结构和内模式的物理结构联系起来,该映像可以保证数据和应用程序的物理独立性。
# 九、关系数据库建模
## ER 图
Entity-Relationship有三个组成部分实体、属性、联系。
### 1. 实体的三种联系
联系包含一对一,一对多,多对多三种。
如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B如果是一对一画两个带箭头的线段如果是多对多画两个不带箭头的线段。下图的 Course 和 Student 是一对多的关系。
<div align="center"> <img src="../pics//292b4a35-4507-4256-84ff-c218f108ee31.jpg"/> </div><br>
### 2. 表示出现多次的关系
一个实体在联系出现几次,就要用几条线连接。下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
<div align="center"> <img src="../pics//8b798007-e0fb-420c-b981-ead215692417.jpg"/> </div><br>
### 3. 联系的多向性
虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。
<div align="center"> <img src="../pics//423f2a40-bee1-488e-b460-8e76c48ee560.png"/> </div><br>
一般只使用二元联系,可以把多元关系转换为二元关系。
<div align="center"> <img src="../pics//de9b9ea0-1327-4865-93e5-6f805c48bc9e.png"/> </div><br>
### 4. 表示子类
用一个三角形和两条线来连接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。
<div align="center"> <img src="../pics//7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg"/> </div><br>
## 十、约束
### 1. 键码
用于唯一表示一个实体。
键码可以由多个属性构成,每个构成键码的属性称为码。
### 2. 单值约束
某个属性的值是唯一的。
### 3. 引用完整性约束
一个实体的属性引用的值在另一个实体的某个属性中存在。
### 4. 域约束
某个属性的值在特定范围之内。
### 5. 一般约束
比如大小约束,数量约束。
# 参考资料
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
- 施瓦茨. 高性能MYSQL(第3版)[M]. 电子工业出版社, 2013.
- [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html)
- [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels)
- [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol)
- [The Nightmare of Locking, Blocking and Isolation Levels!](https://www.slideshare.net/brshristov/the-nightmare-of-locking-blocking-and-isolation-levels-46391666)
- [三级模式与两级映像](http://blog.csdn.net/d2457638978/article/details/48783923)
- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html)
- [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/)
- [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/)
- [浅入浅出 MySQL 和 InnoDB](https://draveness.me/mysql-innodb)
- [fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg](https://tech.meituan.com/innodb-lock.html)

400
notes/正则表达式.md Normal file
View File

@ -0,0 +1,400 @@
<!-- GFM-TOC -->
* [一、概述](#一概述)
* [二、匹配单个字符](#二匹配单个字符)
* [三、匹配一组字符](#三匹配一组字符)
* [四、使用元字符](#四使用元字符)
* [五、重复匹配](#五重复匹配)
* [六、位置匹配](#六位置匹配)
* [七、使用子表达式](#七使用子表达式)
* [八、回溯引用](#八回溯引用)
* [九、前后查找](#九前后查找)
* [十、嵌入条件](#十嵌入条件)
<!-- GFM-TOC -->
# 一、概述
正则表达式用于文本内容的查找和替换。
正则表达式内置于其它语言或者软件产品中,它本身不是一种语言或者软件。
一个问题往往可以用多种正则表达式方案来解决。
[正则表达式在线工具](http://tool.chinaz.com/regex)
# 二、匹配单个字符
正则表达式一般是区分大小写的,但是也有些实现是不区分。
**.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符;
**\\** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。
**正则表达式**
```
nam.
```
**匹配结果**
My **name** is Zheng.
# 三、匹配一组字符
**[ ]** 定义一个字符集合;
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定。字符区间只能用在 [ ] 之间,因此 **-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
**^** 是取非操作,必须在 [ ] 字符集合中使用;
**应用**
匹配以 abc 为开头,并且最后一个字母不为数字的字符串:
**正则表达式**
```
abc[^0-9]
```
**匹配结果**
1. **abcd**
2. abc1
3. abc2
# 四、使用元字符
## 匹配空白字符
| 元字符 | 说明 |
| ------------ | ------------ |
| [\b] | 回退(删除)一个字符 |
| \f | 换页符 |
| \n | 换行符 |
| \r | 回车符 |
| \t | 制表符 |
| \v | 垂直制表符 |
\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n \r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
. 是元字符,前提是没有对它们进行转义; f 和 n 也是元字符,但是前提是对他们进行了转义。
## 匹配特定的字符类别
### 1. 数字元字符
| 元字符 | 说明 |
| ------------ | ------------ |
| \d | 数字字符,等价于 [0-9] |
| \D | 非数字字符,等价于 [^0-9] |
### 2. 字母数字元字符
| 元字符 | 说明 |
| ------------ | ------------ |
| \w | 大小写字母,下划线和数字,等价于 [a-zA-Z0-9\_] |
| \W | 对 \w 取非 |
### 3. 空白字符元字符
| 元字符 | 说明 |
| ------------ | ------------ |
| \s | 任何一个空白字符,等价于 [\f\n\r\t\v] |
| \S | 对 \s 取非 |
\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10 ,等价于 \n也就是它会匹配 \n 。
## 使用 POSIX 字符类
| 字符类 | 说明 |
| --- | --- |
| [:alnum:] | 字母数字字符 |
| [:alpha:] | 字母字符 |
| [:cntrl:] | 控制字符 |
| [:digit:] | 数字字符 |
| [:graph:] | 非空白字符 ( 非空格、控制字符等 ) |
| [:lower:] | 小写字母 |
| [:print:] | 与 [:graph:] 相似,但是包含空格字符 |
| [:punct:] | 标点字符 |
| [:space:] | 所有的空白字符 ( 换行符、空格、制表符 ) |
| [:upper:] | 大写字母 |
| [:xdigit:] | 允许十六进制的数字 (0-9a-fA-F) |
并不是所有正则表达式实现都支持 POSIX 字符类,也不一定使用它。
使用时需要用两对方括号,例如 [[:alpha:]]。
# 五、重复匹配
**\+** 匹配 1 个或者多个字符, **\*** 匹配 0 个或者多个,**?** 匹配 0 个或者 1 个。
**应用**
匹配邮箱地址。
**正则表达式**
```
[\w.]+@\w+.\w+
```
[\w.] 匹配的是字母数字或者 . ,在其后面加上 + ,表示匹配多次。在字符集合 [ ] 里,. 不是元字符;
**匹配结果**
**abc.def<span>@</span>qq.com**
为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。
```
\w+@\w+.\w+
[\w]+@[\w]+.[\w]+
```
**{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符;
\* 和 + 都是贪婪型元字符,会匹配最多的内容,在元字符后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。
**正则表达式**
```
a.+c
```
由于 + 是贪婪型的,因此 .+ 会匹配更可能多的内容,所以会把整个 abcabcabc 文本都匹配,而不是只匹配前面的 abc 文本。用懒惰型可以实现匹配前面的。
**匹配结果**
**abcabcabc**
# 六、位置匹配
## 单词边界
**\b** 可以匹配一个单词的边界,边界是指位于 \w 和 \W 之间的位置;**\B** 匹配一个不是单词边界的位置。
\b 只匹配位置,不匹配字符,因此 \babc\b 匹配出来的结果为 3 个字符。
## 字符串边界
**^** 匹配整个字符串的开头,**$** 匹配结尾。
^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。
使用 (?m) 来打开分行匹配模式,在该模式下,换行被当做字符串的边界。
**应用**
匹配代码中以 // 开始的注释行
**正则表达式**
```
(?m)^\s*//.*$
```
如果没用 (?m),则只会匹配 // 注释 1 以及之后的所有内容,因为 * 是贪婪型的。用了分行匹配模式之后,换行符被当成是字符串分隔符,因此能正确匹配出两个注释内容。
**匹配结果**
1. public void fun() {
2. &nbsp;&nbsp;&nbsp;&nbsp; **// 注释 1**
3. &nbsp;&nbsp;&nbsp;&nbsp; int a = 1;
4. &nbsp;&nbsp;&nbsp;&nbsp; int b = 2;
5. &nbsp;&nbsp;&nbsp;&nbsp; **// 注释 2**
6. &nbsp;&nbsp;&nbsp;&nbsp; int c = a + b;
7. }
# 七、使用子表达式
使用 **( )** 定义一个子表达式。子表达式的内容可以当成一个独立元素,即可以将它看成一个字符,并且使用 * 等元字符。
子表达式可以嵌套,但是嵌套层次过深会变得很难理解。
**正则表达式**
```
(ab) {2,}
```
**匹配结果**
**ababab**
**|** 是或元字符,它把左边和右边所有的部分都看成单独的两个部分,两个部分只要有一个匹配就行。
```
(19|20)\d{2}
```
**匹配结果**
1. **1900**
2. **2010**
3. 1020
**应用**
匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
1. 一位或者两位的数字
2. 1 开头的三位数
3. 2 开头,第 2 位是 0-4 的三位数
4. 25 开头,第 3 位是 0-5 的三位数
**正则表达式**
```
(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.) {3}(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5])))
```
**匹配结果**
1. **192.168.0.1**
2. 555.555.555.555
# 八、回溯引用
回溯引用使用 **\n** 来引用某个子表达式,其中 n 代表的是子表达式的序号,从 1 开始。它和子表达式匹配的内容一致,比如子表达式匹配到 abc ,那么回溯引用部分也需要匹配 abc 。
**应用**
匹配 HTML 中合法的标题元素。
**正则表达式**
\1 将回溯引用子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。
```
<(h[1-6])>\w*?</\1>
```
**匹配结果**
1. **&lt;h1>x&lt;/h1>**
2. **&lt;h2>x&lt;/h2>**
3. &lt;h3>x&lt;/h1>
## 替换
需要用到两个正则表达式。
**应用**
修改电话号码格式。
**文本**
313-555-1234
**查找正则表达式**
```
(\d{3})(-)(\d{3})(-)(\d{4})
```
**替换正则表达式**
在第一个子表达式查找的结果加上 () ,然后加一个空格,在第三个和第五个字表达式查找的结果中间加上 - 进行分隔。
```
($1) $3-$5
```
**结果**
(313) 555-1234
## 大小写转换
| 元字符 | 说明 |
| ---| ---|
| \l | 把下个字符转换为小写 |
| \u| 把下个字符转换为大写 |
| \L | 把\L 和\E 之间的字符全部转换为小写 |
| \U | 把\U 和\E 之间的字符全部转换为大写 |
| \E | 结束\L 或者\U |
**应用**
把文本的第二个和第三个字符转换为大写。
**文本**
abcd
**查找**
```
(\w)(\w{2})(\w)
```
**替换**
```
$1\U$2\E$3
```
**结果**
aBCd
# 九、前后查找
前后查找规定了匹配的内容首尾应该匹配的内容,但是又不包含首尾匹配的内容。向前查找用 **?=** 来定义,它规定了尾部匹配的内容,这个匹配的内容在 ?= 之后定义。所谓向前查找,就是规定了一个匹配的内容,然后以这个内容为尾部向前面查找需要匹配的内容。向后匹配用 ?<= 定义。
**应用**
查找出邮件地址 @ 字符前面的部分。
**正则表达式**
```
\w+(?=@)
```
**结果**
**abc** @qq.com
对向前和向后查找取非,只要把 = 替换成 ! 即可,比如 (?=) 替换成 (?!) 。取非操作使得匹配那些首尾不符合要求的内容。
# 十、嵌入条件
## 回溯引用条件
条件判断为某个子表达式是否匹配,如果匹配则需要继续匹配条件表达式后面的内容。
**正则表达式**
子表达式 (\\() 匹配一个左括号,其后的 ? 表示匹配 0 个或者 1 个。 ?(1) 为条件,当子表达式 1 匹配时条件成立,需要执行 \) 匹配,也就是匹配右括号。
```
(\()?abc(?(1)\))
```
**结果**
1. **(abc)**
2. **abc**
3. (abc
## 前后查找条件
条件为定义的首尾是否匹配,如果匹配,则继续执行后面的匹配。注意,首尾不包含在匹配的内容中。
**正则表达式**
?(?=-) 为前向查找条件,只有在以 - 为前向查找的结尾能匹配 \d{5} ,才继续匹配 -\d{4} 。
```
\d{5}(?(?=-)-\d{4})
```
**结果**
1. **11111**
2. 22222-
3. **33333-4444**

1621
notes/算法.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,889 @@
<!-- GFM-TOC -->
* [一、 概述](#一-概述)
* [操作系统基本特征](#操作系统基本特征)
* [操作系统基本功能](#操作系统基本功能)
* [系统调用](#系统调用)
* [大内核和微内核](#大内核和微内核)
* [中断分类](#中断分类)
* [二、进程管理](#二进程管理)
* [进程与线程](#进程与线程)
* [进程状态的切换](#进程状态的切换)
* [调度算法](#调度算法)
* [进程同步](#进程同步)
* [经典同步问题](#经典同步问题)
* [进程通信](#进程通信)
* [三、死锁](#三死锁)
* [死锁的必要条件](#死锁的必要条件)
* [死锁的处理方法](#死锁的处理方法)
* [四、内存管理](#四内存管理)
* [虚拟内存](#虚拟内存)
* [分页与分段](#分页与分段)
* [分页系统地址映射](#分页系统地址映射)
* [页面置换算法](#页面置换算法)
* [五、设备管理](#五设备管理)
* [磁盘调度算法](#磁盘调度算法)
* [六、链接](#六链接)
* [编译系统](#编译系统)
* [目标文件](#目标文件)
* [静态链接](#静态链接)
* [动态链接](#动态链接)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、 概述
## 操作系统基本特征
### 1. 并发
并发性是指宏观上在一段时间内能同时运行多个程序,而并行性则指同一时刻能运行多个指令。
并行需要硬件支持,如多流水线或者多处理器。
操作系统通过引入进程和线程,使得程序能够并发运行。
### 2. 共享
共享是指系统中的资源可以供多个并发进程共同使用。
有两种共享方式:互斥共享和同时共享。
互斥共享的资源称为临界资源,例如打印机等,在同一时间只允许一个进程访问,需要用同步机制来实现对临界资源的访问。
### 3. 虚拟
虚拟技术把一个物理实体转换为多个逻辑实体。
主要有两种虚拟技术:时分复用技术和空分复用技术。例如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。
### 4. 异步
异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。
## 操作系统基本功能
### 1. 进程管理
进程控制、进程同步、进程通信、死锁处理、处理机调度等。
### 2. 内存管理
内存分配、地址映射、内存保护与共享和内存扩充等功能。
### 3. 文件管理
文件存储空间的管理、目录管理及文件读写管理和保护等。
### 4. 设备管理
完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率,主要包括缓冲管理、设备分配、设备处理和虛拟设备等功能。
## 系统调用
如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成。
<div align="center"> <img src="../pics//tGPV0.png" width="600"/> </div><br>
Linux 的系统调用主要有以下这些:
| Task | Commands |
| :---: | --- |
| 进程控制 | fork(); exit(); wait(); |
| 进程通信 | pipe(); shmget(); mmap(); |
| 文件操作 | open(); read(); write(); |
| 设备操作 | ioctl(); read(); write(); |
| 信息维护 | getpid(); alarm(); sleep(); |
| 安全 | chmod(); umask(); chown(); |
## 大内核和微内核
### 1. 大内核
大内核是将操作系统功能作为一个紧密结合的整体放到内核。
由于各模块共享信息,因此有很高的性能。
### 2. 微内核
由于操作系统不断复杂,为了实现因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。
因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。
<div align="center"> <img src="../pics//2_14_microkernelArchitecture.jpg"/> </div><br>
## 中断分类
### 1. 外中断
由 CPU 执行指令以外的事件引起,如 I/O 结束中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。
### 2. 异常
由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。
### 3. 陷入
在用户程序中使用系统调用。
# 二、进程管理
## 进程与线程
### 1. 进程
进程是资源分配的基本单位。
进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。
<div align="center"> <img src="../pics//a6ac2b08-3861-4e85-baa8-382287bfee9f.png"/> </div><br>
### 2. 线程
线程是独立调度的基本单位。
一个进程中可以有多个线程,它们共享进程资源。
<div align="center"> <img src="../pics//3cd630ea-017c-488d-ad1d-732b4efeddf5.png"/> </div><br>
### 3. 区别
- 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
- 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。
- 系统开销由于创建或撤销进程时系统都要为之分配或回收资源如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
- 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。
举例QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
## 进程状态的切换
<div align="center"> <img src="../pics//ProcessState.png" width="500"/> </div><br>
- 就绪状态ready等待被调度
- 运行状态running
- 阻塞状态waiting等待资源
应该注意以下内容:
- 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
- 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。
## 调度算法
需要针对不同环境来讨论调度算法。
### 1. 批处理系统中的调度
#### 1.1 先来先服务
> first-come first-serverdFCFS
调度最先进入就绪队列的作业。
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
#### 1.2 短作业优先
> shortest job firstSJF
调度估计运行时间最短的作业。
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。
#### 1.3 最短剩余时间优先
> shortest remaining time nextSRTN
### 2. 交互式系统中的调度
#### 2.1 优先级调度
除了可以手动赋予优先权之外,还可以把响应比作为优先权,这种调度方式叫做高响应比优先调度算法。
响应比 = (等待时间 + 要求服务时间) / 要求服务时间 = 响应时间 / 要求服务时间
这种调度算法主要是为了解决短作业优先调度算法长作业可能会饿死的问题,因为随着等待时间的增长,响应比也会越来越高。
#### 2.2 时间片轮转
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系。因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
#### 2.3 多级反馈队列
<div align="center"> <img src="../pics//042cf928-3c8e-4815-ae9c-f2780202c68f.png"/> </div><br>
如果一个进程需要执行 100 个时间片,如果采用轮转调度算法,那么需要交换 100 次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
### 3. 实时系统中的调度
实时系统要求一个服务请求在一个确定时间内得到响应。
分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
## 进程同步
### 1. 临界区
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。
```html
// entry section
// critical section;
// exit section
```
### 2. 同步与互斥
- 同步:多个进程按一定顺序执行;
- 互斥:多个进程在同一时刻只有一个进程能进入临界区。
### 3. 信号量
信号量Semaphore是一个整型变量可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。
- **down** : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0进程睡眠等待信号量大于 0
- **up** :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。
down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
如果信号量的取值只能为 0 或者 1那么就成为了 **互斥量Mutex** 0 表示临界区已经加锁1 表示临界区解锁。
```c
typedef int semaphore;
semaphore mutex = 1;
void P1() {
down(&mutex);
// 临界区
up(&mutex);
}
void P2() {
down(&mutex);
// 临界区
up(&mutex);
}
```
<font size=3> **使用信号量实现生产者-消费者问题** </font> </br>
问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
为了同步生产者和消费者的行为需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计这里需要使用两个信号量empty 记录空缓冲区的数量full 记录满缓冲区的数量。其中empty 信号量是在生产者进程中使用,当 empty 不为 0 时生产者才可以放入物品full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0此时生产者睡眠。消费者不能进入临界区因为生产者对缓冲区加锁了也就无法执行 up(empty) 操作empty 永远都为 0那么生产者和消费者就会一直等待下去造成死锁。
```c
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer() {
while(TRUE){
int item = produce_item();
down(&empty);
down(&mutex);
insert_item(item);
up(&mutex);
up(&full);
}
}
void consumer() {
while(TRUE){
down(&full);
down(&mutex);
int item = remove_item();
up(&mutex);
up(&empty);
consume_item(item);
}
}
```
### 4. 管程
使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。
```pascal
monitor ProducerConsumer
integer i;
condition c;
procedure insert();
begin
// ...
end;
procedure remove();
begin
// ...
end;
end monitor;
```
管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。
管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
<font size=3> **使用管程实现生成者-消费者问题** </font><br>
```pascal
// 管程
monitor ProducerConsumer
condition full, empty;
integer count := 0;
condition c;
procedure insert(item: integer);
begin
if count = N then wait(full);
insert_item(item);
count := count + 1;
if count = 1 then signal(empty);
end;
function remove: integer;
begin
if count = 0 then wait(empty);
remove = remove_item;
count := count - 1;
if count = N -1 then signal(full);
end;
end monitor;
// 生产者客户端
procedure producer
begin
while true do
begin
item = produce_item;
ProducerConsumer.insert(item);
end
end;
// 消费者客户端
procedure consumer
begin
while true do
begin
item = ProducerConsumer.remove;
consume_item(item);
end
end;
```
## 经典同步问题
生产者和消费者问题前面已经讨论过了。
### 1. 读者-写者问题
允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。
一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。
```c
typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;
void reader() {
while(TRUE) {
down(&count_mutex);
count++;
if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
up(&count_mutex);
read();
down(&count_mutex);
count--;
if(count == 0) up(&data_mutex);
up(&count_mutex);
}
}
void writer() {
while(TRUE) {
down(&data_mutex);
write();
up(&data_mutex);
}
}
```
### 2. 哲学家进餐问题
<div align="center"> <img src="../pics//a9077f06-7584-4f2b-8c20-3a8e46928820.jpg"/> </div><br>
五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起筷子左右的两根筷子,并且一次只能拿起一根筷子。
下面是一种错误的解法,考虑到如果所有哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。
```c
#define N 5
void philosopher(int i) {
while(TRUE) {
think();
take(i); // 拿起左边的筷子
take((i+1)%N); // 拿起右边的筷子
eat();
put(i);
put((i+1)%N);
}
}
```
为了防止死锁的发生,可以设置两个条件:
1. 必须同时拿起左右两个筷子;
2. 只有在两个邻居都没有进餐的情况下才允许进餐。
```c
#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N // 右邻居
#define THINKING 0
#define HUNGRY 1
#define EATING 2
typedef int semaphore;
int state[N]; // 跟踪每个哲学家的状态
semaphore mutex = 1; // 临界区的互斥
semaphore s[N]; // 每个哲学家一个信号量
void philosopher(int i) {
while(TRUE) {
think();
take_two(i);
eat();
put_tow(i);
}
}
void take_two(int i) {
down(&mutex);
state[i] = HUNGRY;
test(i);
up(&mutex);
down(&s[i]);
}
void put_tow(i) {
down(&mutex);
state[i] = THINKING;
test(LEFT);
test(RIGHT);
up(&mutex);
}
void test(i) { // 尝试拿起两把筷子
if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
state[i] = EATING;
up(&s[i]);
}
}
```
## 进程通信
### 1. 进程同步与进程通信的区别
- 进程同步:控制多个进程按一定顺序执行;
- 进程通信:进程间传输信息。
进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。
在进程同步中介绍的信号量也属于进程通信的一种方式,但是属于低级别的进程通信,因为它传输的信息非常小。
### 2. 进程通信方式
#### 2.1 消息传递
操作系统提供了用于通信的通道Channel进程可以通过读写这个通道进行通信。
<div align="center"> <img src="../pics//037c3a0b-332d-434d-a374-f343ef72c8e1.jpg" width="400"/> </div><br>
<font size=3> **(一)管道** </font></br>
写进程在管道的尾端写入数据,读进程在管道的首端读出数据。管道提供了简单的流控制机制,进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
Linux 中管道通过空文件实现。
管道有三种:
1. 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用;
2. 流管道:去除第一个限制,支持双向传输;
3. 命名管道:去除第二个限制,可以在不相关进程之间进行通信。
<div align="center"> <img src="../pics//7f642a65-b167-4c8f-b382-8322c6322b2c.jpg" width="400"/> </div><br>
<font size=3> **(二)消息队列** </font></br>
消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
<div align="center"> <img src="../pics//d49466db-fdd3-4d36-8a86-47dc45c07a1e.jpg" width="400"/> </div><br>
<font size=3> **(三)套接字** </font></br>
套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。
<div align="center"> <img src="../pics//699b4f96-d63f-46ea-a581-2b3d95eceb6a.jpg" width="400"/> </div><br>
#### 2.2 共享内存
操作系统建立一块共享内存,并将其映射到每个进程的地址空间上,进程就可以直接对这块共享内存进行读写。
共享内存是最快的进程通信方式。
<div align="center"> <img src="../pics//7be0abf9-687c-4451-becd-626b0be7ec22.jpg" width="400"/> </div><br>
# 三、死锁
## 死锁的必要条件
<div align="center"> <img src="../pics//c037c901-7eae-4e31-a1e4-9d41329e5c3e.png"/> </div><br>
1. 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
2. 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
3. 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显示地释放。
4. 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
## 死锁的处理方法
### 1. 鸵鸟策略
把头埋在沙子里,假装根本没发生问题。
因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
大多数操作系统,包括 UnixLinux 和 Windows处理死锁问题的办法仅仅是忽略它。
### 2. 死锁检测与死锁恢复
不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。
**(一)每种类型一个资源的死锁检测**
<div align="center"> <img src="../pics//b1fa0453-a4b0-4eae-a352-48acca8fff74.png"/> </div><br>
上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。
图 a 可以抽取出环,如图 b它满足了环路等待条件因此会发生死锁。
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
**(二)每种类型多个资源的死锁检测**
<div align="center"> <img src="../pics//e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png"/> </div><br>
上图中,有三个进程四个资源,每个数据代表的含义如下:
- E 向量:资源总量
- A 向量:资源剩余量
- C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量
- R 矩阵:每个进程请求的资源数量
进程 P<sub>1</sub> 和 P<sub>2</sub> 所请求的资源都得不到满足,只有进程 P<sub>3</sub> 可以,让 P<sub>3</sub> 执行,之后释放 P<sub>3</sub> 拥有的资源,此时 A = (2 2 2 0)。P<sub>2</sub> 可以执行,执行后释放 P<sub>2</sub> 拥有的资源A = (4 2 2 1) 。P<sub>1</sub> 也可以执行。所有进程都可以顺利执行,没有死锁。
算法总结如下:
每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。
1. 寻找一个没有标记的进程 P<sub>i</sub>,它所请求的资源小于等于 A。
2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。
3. 如果没有这样一个进程,算法终止。
**(三)死锁恢复**
- 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
### 3. 死锁预防
在程序运行之前预防发生死锁。
**(一)破坏互斥条件**
例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
**(二)破坏占有和等待条件**
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
**(三)破坏不可抢占条件**
**(四)破坏环路等待**
给资源统一编号,进程只能按编号顺序来请求资源。
### 4. 死锁避免
在程序运行时避免发生死锁。
**(一)安全状态**
<div align="center"> <img src="../pics//ed523051-608f-4c3f-b343-383e2d194470.png"/> </div><br>
图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b运行结束后释放 B此时 Free 变为 5图 c接着以同样的方式运行 C 和 A使得所有进程都能成功运行因此可以称图 a 所示的状态时安全的。
定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。
安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。
**(二)单个资源的银行家算法**
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
<div align="center"> <img src="../pics//d160ec2e-cfe2-4640-bda7-62f53e58b8c0.png"/> </div><br>
上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。
**(三)多个资源的银行家算法**
<div align="center"> <img src="../pics//62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png"/> </div><br>
上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。
检查一个状态是否安全的算法如下:
- 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行那么系统将会发生死锁状态是不安全的。
- 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
- 重复以上两步,直到所有进程都标记为终止,则状态时安全的。
如果一个状态不是安全的,也需要拒绝进入这个状态。
# 四、内存管理
## 虚拟内存
每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
## 分页与分段
### 1. 分页
大部分虚拟内存系统都使用分页技术。把由程序产生的地址称为虚拟地址,它们构成了一个虚拟地址空间。例如有一台计算机可以产生 16 位地址,它的虚拟地址空间为 0\~64K然而计算机只有 32KB 的物理内存,因此虽然可以编写 64KB 的程序,但它们不能被完全调入内存运行。
<div align="center"> <img src="../pics//7b281b1e-0595-402b-ae35-8c91084c33c1.png"/> </div><br>
虚拟地址空间划分成固定大小的页,在物理内存中对应的单元称为页框,页和页框大小通常相同,它们之间通过页表进行映射。
程序最开始只将一部分页调入页框中,当程序引用到没有在页框的页时,产生缺页中断,进行页面置换,按一定的原则将一部分页框换出,并将页调入。
### 2. 分段
<div align="center"> <img src="../pics//22de0538-7c6e-4365-bd3b-8ce3c5900216.png"/> </div><br>
上图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。
<div align="center"> <img src="../pics//e0900bb2-220a-43b7-9aa9-1d5cd55ff56e.png"/> </div><br>
分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。
每个段都需要程序员来划分。
### 3. 段页式
用分段方法来分配和管理虚拟存储器。程序的地址空间按逻辑单位分成基本独立的段,而每一段有自己的段名,再把每段分成固定大小的若干页。
用分页方法来分配和管理实存。即把整个主存分成与上述页大小相等的存储块,可装入作业的任何一页。
程序对内存的调入或调出是按页进行的,但它又可按段实现共享和保护。
### 4. 分页与分段区别
- 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。
- 地址空间的维度:分页是一维地址空间,分段是二维的。
- 大小是否可以改变:页的大小不可变,段的大小可以动态改变。
- 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
## 分页系统地址映射
- 内存管理单元MMU管理着虚拟地址空间和物理内存的转换。
- 页表Page table虚拟地址空间和页框物理内存空间的映射表。例如下图中页表的第 0 个表项为 010表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。
下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。因此对于虚拟地址0010 000000000100前 4 位是用来存储页面号,而后 12 位存储在页中的偏移量。
0010 000000000100根据前 4 位得到页号为 2读取表项内容为110 1它的前 3 为为页框号,最后 1 位表示该页在内存中。最后映射得到物理内存地址为110 000000000100
<div align="center"> <img src="../pics//cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png" width="500"/> </div><br>
## 页面置换算法
在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。
### 1. 最佳
> Optimal
所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。
是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。
举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列:
<div align="center"><img src="https://latex.codecogs.com/gif.latex?70120304230321201701"/></div> <br>
开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。
### 2. 先进先出
> FIFO, First In First Out
所选择换出的页面是最先进入的页面。
该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。
### 3. 最近最久未使用
> LRU, Least Recently Used
虽然无法知道将来要使用的页面情况但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。
可以用栈来实现该算法,栈中存储页面的页面号。当进程访问一个页面时,将该页面的页面号从栈移除,并将它压入栈顶。这样,最近被访问的页面总是在栈顶,而最近最久未使用的页面总是在栈底。
<div align="center"><img src="https://latex.codecogs.com/gif.latex?47071012126"/></div> <br>
<div align="center"> <img src="../pics//eb859228-c0f2-4bce-910d-d9f76929352b.png"/> </div><br>
### 4. 时钟
> Clock
需要用到一个访问位,当一个页面被访问时,将访问位置为 1。
首先,将内存中的所有页面链接成一个循环队列,当缺页中断发生时,检查当前指针所指向页面的访问位,如果访问位为 0就将该页面换出否则将该页的访问位设置为 0给该页面第二次的机会移动指针继续检查。
<div align="center"> <img src="../pics//5f5ef0b6-98ea-497c-a007-f6c55288eab1.png"/> </div><br>
# 五、设备管理
## 磁盘调度算法
当多个进程同时请求访问磁盘时,需要进行磁盘调度来控制对磁盘的访问。
磁盘调度的主要目标是使磁盘的平均寻道时间最少。
### 1. 先来先服务
> FCFS, First Come First Served
根据进程请求访问磁盘的先后次序来进行调度。优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。
### 2. 最短寻道时间优先
> SSTF, Shortest Seek Time First
要求访问的磁道与当前磁头所在磁道距离最近的优先进行调度。这种算法并不能保证平均寻道时间最短,但是比 FCFS 好很多。
### 3. 扫描算法
> SCAN
SSTF 会出现饥饿现象。考虑以下情况,新进程请求访问的磁道与磁头所在磁道的距离总是比一个在等待的进程来的近,那么等待的进程会一直等待下去。
SCAN 算法在 SSTF 算法之上考虑了磁头的移动方向,要求所请求访问的磁道在磁头当前移动方向上才能够得到调度。因为考虑了移动方向,那么一个进程请求访问的磁道一定会得到调度。
当一个磁头自里向外移动时,移到最外侧会改变移动方向为自外向里,这种移动的规律类似于电梯的运行,因此又常称 SCAN 算法为电梯调度算法。
### 4. 循环扫描算法
> CSCAN
CSCAN 对 SCAN 进行了改动,要求磁头始终沿着一个方向移动。
# 六、链接
## 编译系统
以下是一个 hello.c 程序:
```c
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
```
在 Unix 系统上,由编译器把源文件转换为目标文件。
```bash
gcc -o hello hello.c
```
这个过程大致如下:
<div align="center"> <img src="../pics//b396d726-b75f-4a32-89a2-03a7b6e19f6f.jpg" width="800"/> </div><br>
1. 预处理阶段:处理以 # 开头的预处理命令;
2. 编译阶段:翻译成汇编程序;
3. 汇编阶段:将汇编程序翻译可重定向目标程序,它是二进制的;
4. 链接阶段:将可重定向目标程序和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标程序。
## 目标文件
1. 可执行目标文件:可以直接在内存中执行;
2. 可重定向目标文件:可与其他可重定向目标文件在链接阶段合并,创建一个可执行目标文件;
3. 共享目标文件:可以在运行时被动态加载进内存并链接;
## 静态链接
静态连接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:
1. 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用于一个符号定义关联起来。
2. 重定位:编译器和汇编器生成从地址 0 开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
<div align="center"> <img src="../pics//47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg"/> </div><br>
## 动态链接
静态库有以下两个问题:
- 当静态库更新时那么整个程序都要重新进行链接;
- 对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。
共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示Windows 系统上它们被称为 DLL。它具有以下特点
1. 在给定的文件系统中一个库只有一个 .so 文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
2. 在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。
<div align="center"> <img src="../pics//76dc7769-1aac-4888-9bea-064f1caa8e77.jpg"/> </div><br>
# 参考资料
- Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014.
- 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001.
- Bryant, R. E., & OHallaron, D. R. (2004). 深入理解计算机系统.
- [Operating System Notes](https://applied-programming.github.io/Operating-Systems-Notes/)
- [进程间的几种通信方式](http://blog.csdn.net/yufaw/article/details/7409596)
- [Operating-System Structures](https://www.cs.uic.edu/\~jbell/CourseNotes/OperatingSystems/2_Structures.html)
- [Processes](http://cse.csusb.edu/tongyu/courses/cs460/notes/process.php)
- [Inter Process Communication Presentation[1]](https://www.slideshare.net/rkolahalam/inter-process-communication-presentation1)

888
notes/计算机网络.md Normal file
View File

@ -0,0 +1,888 @@
<!-- GFM-TOC -->
* [一、概述](#一概述)
* [网络的网络](#网络的网络)
* [ISP](#isp)
* [互联网的组成](#互联网的组成)
* [主机之间的通信方式](#主机之间的通信方式)
* [电路交换与分组交换](#电路交换与分组交换)
* [时延](#时延)
* [计算机网络体系结构*](#计算机网络体系结构)
* [二、物理层](#二物理层)
* [通信方式](#通信方式)
* [带通调制](#带通调制)
* [信道复用技术](#信道复用技术)
* [三、数据链路层](#三数据链路层)
* [三个基本问题](#三个基本问题)
* [点对点信道 - PPP 协议](#点对点信道---ppp-协议)
* [局域网的拓扑](#局域网的拓扑)
* [广播信道- CSMA/CD 协议*](#广播信道--csmacd-协议)
* [扩展局域网*](#扩展局域网)
* [MAC 层*](#mac-层)
* [四、网络层 *](#四网络层-)
* [网际协议 IP 概述](#网际协议-ip-概述)
* [IP 数据报格式](#ip-数据报格式)
* [IP 地址编址](#ip-地址编址)
* [IP 地址和 MAC 地址](#ip-地址和-mac-地址)
* [地址解析协议 ARP](#地址解析协议-arp)
* [路由器的结构](#路由器的结构)
* [路由器分组转发流程](#路由器分组转发流程)
* [路由选择协议](#路由选择协议)
* [网际控制报文协议 ICMP](#网际控制报文协议-icmp)
* [分组网间探测 PING](#分组网间探测-ping)
* [IP 多播](#ip-多播)
* [虚拟专用网 VPN](#虚拟专用网-vpn)
* [网络地址转换 NAT](#网络地址转换-nat)
* [五、运输层*](#五运输层)
* [UDP 和 TCP 的特点](#udp-和-tcp-的特点)
* [UDP 首部格式](#udp-首部格式)
* [TCP 首部格式](#tcp-首部格式)
* [TCP 的三次握手](#tcp-的三次握手)
* [TCP 的四次挥手](#tcp-的四次挥手)
* [TCP 滑动窗口](#tcp-滑动窗口)
* [TCP 可靠传输](#tcp-可靠传输)
* [TCP 流量控制](#tcp-流量控制)
* [TCP 拥塞控制](#tcp-拥塞控制)
* [六、应用层*](#六应用层)
* [域名系统 DNS](#域名系统-dns)
* [文件传输协议 FTP](#文件传输协议-ftp)
* [远程终端协议 TELNET](#远程终端协议-telnet)
* [万维网 WWW](#万维网-www)
* [电子邮件协议](#电子邮件协议)
* [动态主机配置协议 DHCP](#动态主机配置协议-dhcp)
* [点对点传输 P2P](#点对点传输-p2p)
* [Web 页面请求过程](#web-页面请求过程)
* [常用端口](#常用端口)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、概述
## 网络的网络
网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。
<div align="center"> <img src="../pics//77f81379-3987-4036-8d7c-93a4dcf7b05d.jpg" width="600"/> </div><br>
## ISP
互联网服务提供商 ISP 可以从互联网管理机构获得许多 IP 地址,同时拥有通信线路以及路由器等联网设备,个人或机构向 ISP 缴纳一定的费用就可以接入互联网。
目前的互联网是一种多层次 ISP 结构ISP 根据覆盖面积的大小分为主干 ISP、地区 ISP 和本地 ISP。
互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。
<div align="center"> <img src="../pics//02986f62-c641-44a8-a55f-983581490e0c.png" width="800"/> </div><br>
## 互联网的组成
1. 边缘部分:所有连接在互联网上的主机,用户可以直接使用;
2. 核心部分:由大量的网络和连接这些网络的路由器组成,为边缘部分的主机提供服务。
<div align="center"> <img src="../pics//10f5e35b-1c71-4717-9e80-47f259702642.jpg" width="600"/> </div><br>
## 主机之间的通信方式
1. 客户-服务器C/S客户是服务的请求方服务器是服务的提供方。
2. 对等P2P不区分客户和服务器。
## 电路交换与分组交换
<div align="center"> <img src="../pics//d1f81ac3-9fdb-4371-a49d-ca84917aa89f.jpg" width="800"/> </div><br>
### 1. 电路交换
电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%。
### 2. 报文交换
报文交换用于邮局通信系统,邮局接收到一份报文之后,先存储下来,然后把相同目的地的报文一起转发到下一个目的地,这个过程就是存储转发过程。
### 3. 分组交换
分组交换也使用了存储转发,但是转发的是分组而不是报文。把整块数据称为一个报文,由于一个报文可能很长,需要先进行切分,来满足分组能处理的大小。在每个切分的数据前面加上首部之后就成为了分组,首部包含了目的地址和源地址等控制信息。
<div align="center"> <img src="../pics//94589319-975f-490b-8bae-90b3a4953559.png" width="600"/> </div><br>
存储转发允许在一条传输线路上传送多个主机的分组,也就是说两个用户之间的通信不需要占用端到端的线路资源。
相比于报文交换,由于分组比报文更小,因此分组交换的存储转发速度更加快速。
## 时延
总时延 = 发送时延 + 传播时延 + 处理时延 + 排队时延
<div align="center"> <img src="../pics//3939369b-3a4a-48a0-b9eb-3efae26dd400.png" width="800"/> </div><br>
### 1. 发送时延
主机或路由器发送数据帧所需要的时间。
<div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(bit)}{v(bit/s)}"/></div> <br>
其中 l 表示数据帧的长度v 表示发送速率。
### 2. 传播时延
电磁波在信道中传播一定的距离需要花费的时间,电磁波传播速度接近光速。
<div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(m)}{v(m/s)}"/></div> <br>
其中 l 表示信道长度v 表示电磁波在信道上的传播速率。
### 3. 处理时延
主机或路由器收到分组时进行处理所需要的时间,例如分析首部、从分组中提取数据部、进行差错检验或查找适当的路由等。
### 4. 排队时延
分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。
## 计算机网络体系结构*
<div align="center"> <img src="../pics//426df589-6f97-4622-b74d-4a81fcb1da8e.png" width="800"/> </div><br>
### 1. 七层协议
如图 a 所示,其中表示层和会话层用途如下:
1. 表示层:信息的语法、语义以及它们的关联,如加密解密、转换翻译、压缩解压缩;
2. 会话层:不同机器上的用户之间建立及管理会话。
### 2. 五层协议
1. 应用层:为特定应用程序提供数据传输服务,例如 HTTP、DNS 等。数据单位为报文。
2. 运输层:提供的是进程间的通用数据传输服务。由于应用层协议很多,定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP提供面向连接、可靠的数据传输服务数据单位为报文段用户数据报协议 UDP提供无连接、尽最大努力的数据传输服务数据单位为用户数据报。TCP 主要提供完整性服务UDP 主要提供及时性服务。
3. 网络层:为主机之间提供数据传输服务,而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。
4. 数据链路层:网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的结点提供服务。数据链路层把网络层传来的分组封装成帧。
5. 物理层:考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
### 3. 数据在各层之间的传递过程
在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。
<div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg" width="800"/> </div><br>
### 4. TCP/IP 体系结构
它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。
现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。
<div align="center"> <img src="../pics//45e0e0bf-386d-4280-a341-a0b9496c7674.png" width="400"/> </div><br>
TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中占用举足轻重的地位。
<div align="center"> <img src="../pics//d4eef1e2-5703-4ca4-82ab-8dda93d6b81f.png" width="500"/> </div><br>
# 二、物理层
## 通信方式
1. 单向通信,又称为单工通信;
2. 双向交替通信,又称为半双工通信;
3. 双向同时通信,又称为全双工通信。
## 带通调制
模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。
<div align="center"> <img src="../pics//d2c55c84-aa1f-43c1-bd97-457bcb7816b3.png" width=""/> </div><br>
## 信道复用技术
### 1. 频分复用、时分复用
频分复用的所有用户在相同的时间占用不同的频率带宽资源;时分复用的所有用户在不同的时间占用相同的频率带宽资源。
使用这两种方式进行通信,在通信的过程中用户会一直占用一部分信道资源。但是由于计算机数据的突发性质,没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。
<div align="center"> <img src="../pics//543d47a1-f0dd-414f-b23c-0c142c814854.png" width="500"/> </div><br>
### 2. 统计时分复用
是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送。
<div align="center"> <img src="../pics//29058e09-bb72-4040-a73d-4c497895e9ce.jpg" width="600"/> </div><br>
### 3. 波分复用
光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。
<div align="center"> <img src="../pics//78534153-88d1-4f83-a6e0-59064dbdc43a.png" width="600"/> </div><br>
### 4. 码分复用
为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/><img src="https://latex.codecogs.com/gif.latex?\vec{T}"/>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{T}=0"/></div> <br>
为了方便,取 m=8设码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。
在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{S}=1"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{S'}=-1"/></div> <br>
其中 <img src="https://latex.codecogs.com/gif.latex?\vec{S'}"/><img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 的反码。
利用上面的式子我们知道,当接收端使用码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1结果为 -1 的是用户发送的比特 0。
码分复用需要发送的数据量为原先的 m 倍。
<div align="center"> <img src="../pics//0042edad-8e3b-4279-bd93-6906fcd1b640.jpg" width=""/> </div><br>
# 三、数据链路层
## 三个基本问题
### 1. 封装成帧
将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。
<div align="center"> <img src="../pics//3402d1c0-7020-4249-9a7f-12ea2ea6adf7.jpg" width=""/> </div><br>
### 2. 透明传输
透明表示一个实际存在的事物看起来好像不存在一样。
帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符,如果出现转移字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。
<div align="center"> <img src="../pics//4146e14b-56b9-433c-8e3d-74b1b325399c.jpg" width=""/> </div><br>
### 3. 差错检测
目前数据链路层广泛使用了循环冗余检验CRC来检查比特差错。
## 点对点信道 - PPP 协议
互联网用户通常需要连接到某个 ISP 之后才能接入到互联网PPP 协议就是用户计算机和 ISP 进行通信时所使用的数据链路层协议。
<div align="center"> <img src="../pics//8393f520-d824-44ea-a5f3-1c1a73d735fb.jpg" width=""/> </div><br>
在 PPP 的帧中
- F 字段为帧的定界符
- A 和 C 字段暂时没有意义
- FCS 字段是使用 CRC 的检验序列
- 信息部分的长度不超过 1500
<div align="center"> <img src="../pics//0f39c274-b79c-4e83-8c7c-94fc2747832d.jpg" width=""/> </div><br>
## 局域网的拓扑
<div align="center"> <img src="../pics//8b15e36f-69b4-46b6-a07c-7234ac7c7927.jpg" width=""/> </div><br>
## 广播信道- CSMA/CD 协议*
在广播信道上,同一时间只能允许一台计算机发送数据。
CSMA/CD 表示载波监听多点接入 / 碰撞检测。
- **多点接入** :说明这是总线型网络,许多计算机以多点的方式连接到总线上。
- **载波监听** :每个站都必须不停地监听信道。在发送前,如果检听信道正在使用,就必须等待。
- **碰撞检测** :在发送中,如果监听 到信道已有其它站正在发送数据,就表示发生了碰撞。虽然每一个站在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。
<div align="center"> <img src="../pics//f9ed4da5-0032-41e6-991a-36d995ec28fd.png" width=""/> </div><br>
记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 **争用期** 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。
当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 **截断二进制指数退避算法** 来确定,从离散的整数集合 {0, 1, .., (2<sup>k</sup>-1)} 中随机取出一个数,记作 r然后取 r 倍的争用期作为重传等待时间。
## 扩展局域网*
### 1. 在物理层进行扩展
使用集线器进行扩展。
集线器的主要功能是对接收到的信号进行放大,以扩大网络的传输距离。
集线器不能根据 MAC 地址进行转发,而是以广播的方式发送数据帧。
集线器是一种共享式的传输设备,意味着同一时刻只能传输一组数据帧。
<div align="center"> <img src="../pics//823cdab7-3779-4e3a-a951-dc2d154e0ee6.jpg" width=""/> </div><br>
### 2. 在链路层进行扩展
最开始使用的是网桥,它收到一个帧时,根据帧的 MAC 地址,查找网桥中的地址表,确定将帧转发的接口。
网桥不是共享式设备,因此性能比集线器这种共享式设备更高。
交换机的问世很快就淘汰了网桥,它实质上是一个多接口网桥,而网桥是两接口。交换机的每个接口都能直接与一个主机或者另一个交换机相连,并且一般都工作在全双工方式。
交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B先查交换表此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧,主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1因此就把帧发送给主机 A同时交换机添加主机 B 到接口 3 的映射。
<div align="center"> <img src="../pics//c9cfcd20-c901-435f-9a07-3e46830c359f.jpg" width=""/> </div><br>
### 3. 虚拟局域网
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息,例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
<div align="center"> <img src="../pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png" width=""/> </div><br>
## MAC 层*
MAC 地址是 6 字节48 位)的地址,用于唯一标识网络适配器(网卡),一台主机拥有多少个适配器就有多少个 MAC 地址,例如笔记本电脑普遍存在无线网络适配器和有线网络适配器。
<div align="center"> <img src="../pics//50d38e84-238f-4081-8876-14ef6d7938b5.jpg" width=""/> </div><br>
- **类型** :标记上层使用的协议;
- **数据** :长度在 46-1500 之间,如果太小则需要填充;
- **FCS** :帧检验序列,使用的是 CRC 检验方法;
- **前同步码** :只是为了计算 FCS 临时加入的,计算结束之后会丢弃。
# 四、网络层 *
## 网际协议 IP 概述
因为网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务。
使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。
<div align="center"> <img src="../pics//fe3d224c-8ffd-40f9-85b1-86ffe1393f6c.jpg" width=""/> </div><br>
与 IP 协议配套使用的还有三个协议:
1. 地址解析协议 ARPAddress Resolution Protocol
2. 网际控制报文协议 ICMPInternet Control Message Protocol
3. 网际组管理协议 IGMPInternet Group Management Protocol
<div align="center"> <img src="../pics//163cf8b4-5f30-46c9-af00-316a71b3c890.jpg" width=""/> </div><br>
## IP 数据报格式
<div align="center"> <img src="../pics//8681db55-0873-434b-aa98-83d07e8392ae.jpg" width=""/> </div><br>
- **版本** : 有 4IPv4和 6IPv6两个值
- **首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选部分的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
- **区分服务** : 用来获得更好的服务,一般情况下不使用。
- **总长度** : 包括首部长度和数据部分长度。
- **标识** : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。
- **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。
<div align="center"> <img src="../pics//45c86855-9b18-4cf4-a9a7-f8b6eb78d133.png" width=""/> </div><br>
- **生存时间** TTL它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位当 TTL 为 0 时就丢弃数据报。
- **协议** :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。
- **首部检验和** :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。
## IP 地址编址
IP 地址的编址方式经历了三个历史阶段:
1. 分类;
2. 子网划分;
3. 无分类。
### 1. 分类
由两部分组成,网络号和主机号,其中不同类别具有不同的网络号长度,并且是固定的。
IP 地址 ::= {< 网络号 >, < 主机号 >}
<div align="center"> <img src="../pics//2ddd6132-60be-4a72-9daa-3d9756191f4a.png" width=""/> </div><br>
### 2. 子网划分
通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。注意,外部网络看不到子网的存在。
IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >}
要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000也就是 255.255.192.0。
### 3. 无分类
无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。
IP 地址 ::= {< 网络前缀号 >, < 主机号 >}
CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。
CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。
一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 **构成超网**
在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。
## IP 地址和 MAC 地址
网络层实现主机之间的通信而链路层实现具体每段链路之间的通信。因此在通信过程中IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。
<div align="center"> <img src="../pics//86b71296-0d1e-4a63-bcd9-54955b6b781b.jpg" width=""/> </div><br>
## 地址解析协议 ARP
实现由 IP 地址得到 MAC 地址。
<div align="center"> <img src="../pics//1b4d6737-d834-46ed-8f9d-6f123e29c8dd.jpg" width=""/> </div><br>
每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。
如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到硬件地址的映射。
<div align="center"> <img src="../pics//8bc6fc2c-d198-4759-b06c-18d94d851e97.png" width=""/> </div><br>
## 路由器的结构
路由器从功能上可以划分为两大部分:路由选择和分组转发。
分组转发部分由三部分组成:交换结构、一组输入端口和一组输出端口。
<div align="center"> <img src="../pics//3a676c54-b559-4466-9b21-eb10f1e25879.jpg" width=""/> </div><br>
交换结构的交换网络有以下三种实现方式:
<div align="center"> <img src="../pics//7f82fd18-7f16-4125-ada6-bb6b795b4fda.png" width=""/> </div><br>
## 路由器分组转发流程
1. 从数据报的首部提取目的主机的 IP 地址 D得到目的网络地址 N。路由表项是网络号而不是 IP 地址,这样做大大减少了路由表条目数量);
2. 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付;
3. 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器;
4. 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器;
5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
6. 报告转发分组出错。
<div align="center"> <img src="../pics//8d211911-0e62-4190-ab00-d8610adec4a0.jpg" width=""/> </div><br>
## 路由选择协议
互联网使用的路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。
互联网可以划分为许多较小的自治系统 AS一个 AS 可以使用一种和别的 AS 不同的路由选择协议。
可以把路由选择协议划分为两大类:
1. 内部网关协议 IGPInterior Gateway Protocol在 AS 内部使用,如 RIP 和 OSPF。
2. 外部网关协议 EGPExternal Gateway Protocol在 AS 之间使用,如 BGP。
<div align="center"> <img src="../pics//e0be6970-5b0e-44a2-bc71-df4d61c42b8f.jpg" width=""/> </div><br>
### 1. 内部网关协议 RIP
RIP 是一种分布式的基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1跳数最多为 15超过 15 表示不可达。
RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。
距离向量算法:
1. 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X并把所有的距离字段加 1
2. 对修改后的 RIP 报文中的每一个项目,进行以下步骤:
- 若原来的路由表中没有目的网络 N则把该项目添加到路由表中
- 否则:若下一跳路由器地址是 X则把收到的项目替换原来路由表中的项目否则若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P新表项为 Net2, 4, X则更新否则什么也不做。
3. 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。
RIP 协议实现简单,开销小,但是 RIP 能使用的最大距离为 15限制了网络的规模。并且当网络出现故障时要经过比较长的时间才能将此消息传送到所有路由器。
### 2. 内部网关协议 OSPF
开放最短路径优先 OSPF是为了克服 RIP 的缺点而开发出来的。
开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。
OSPF 具有以下特点:
- 向本自治系统中的所有路由器发送信息,这种方法是洪泛法。
- 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。
- 只有当链路状态发生变化时,路由器才会发送信息。
所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIPOSPF 的更新过程收敛的很快。
### 3. 外部网关协议 BGP
AS 之间的路由选择很困难,主要是互联网规模很大。并且各个 AS 内部使用不同的路由选择协议,就无法准确定义路径的度量。并且 AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。
BGP 只能寻找一条比较好的路由,而不是最佳路由。它采用路径向量路由选择协议。
每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。
<div align="center"> <img src="../pics//eb6271de-22c9-4f4b-8b31-eab1f560efac.png" width=""/> </div><br>
## 网际控制报文协议 ICMP
ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。
<div align="center"> <img src="../pics//9b5e0fa0-9274-4219-a3a9-84fbb509c735.jpg" width=""/> </div><br>
ICMP 报文分为差错报告报文和询问报文。
<div align="center"> <img src="../pics//6e11b122-95ce-4869-bf7d-3b0d7591707e.jpg" width=""/> </div><br>
## 分组网间探测 PING
PING 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。
PING 的过程:
1. PING 同一个网段的主机:查找目的主机的 MAC 地址,然后直接交付。如果无法查找到 MAC 地址,就要进行一次 ARP 请求。
2. PING 不同网段的主机:发送到网关让其进行转发。同样要发送到网关也需要通过查找网关的 MAC 地址,根据 MAC 地址进行转发。
## IP 多播
在一对多的通信中,多播不需要将分组复制多份,从而大大节约网络资源。
<div align="center"> <img src="../pics//c77b6a18-dfac-42a2-ac89-7e99481275dc.jpg" width=""/> </div><br>
## 虚拟专用网 VPN
由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。
有三个专用地址块:
1. 10.0.0.0 \~ 10.255.255.255
2. 172.16.0.0 \~ 172.31.255.255
3. 192.168.0.0 \~ 192.168.255.255
VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指“好像是”,而实际上并不是,它有经过公用的互联网。
下图中,场所 A 和 B 的通信部经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。
<div align="center"> <img src="../pics//bf4ed077-d481-4db7-9e7a-85d841a5a8c3.jpg" width=""/> </div><br>
## 网络地址转换 NAT
专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。
在以前NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把运输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。
<div align="center"> <img src="../pics//0f31bc7a-d60b-48a6-8e3f-597708369e52.png" width=""/> </div><br>
# 五、运输层*
网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。
运输层提供了应用进程间的逻辑通信。运输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看见的好像在两个运输层实体之间有一条端到端的逻辑通信信道。
## UDP 和 TCP 的特点
- 用户数据包协议 UDPUser Datagram Protocol是无连接的尽最大可能交付没有拥塞控制面向报文对于应用程序传下来的报文不合并也不拆分只是添加 UDP 首部)。
- 传输控制协议 TCPTransmission Control Protocol 是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块)
## UDP 首部格式
<div align="center"> <img src="../pics//bd6c05f3-02ee-4c8a-b374-40c87154a898.jpg" width=""/> </div><br>
首部字段只有 8 个字节包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。
## TCP 首部格式
<div align="center"> <img src="../pics//21a00b02-c0a6-4bcd-9af0-5ec6bb66e34c.jpg" width=""/> </div><br>
- **序号** :用于对字节流进行编号,例如序号为 301表示第一个字节的编号为 301如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
- **确认号** :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701B 发送给 A 的确认报文段中确认号就为 701。
- **数据偏移** :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
- **确认 ACK** :当 ACK=1 时确认号字段有效否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
- **同步 SYN** :在连接建立时用来同步序号。当 SYN=1ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1ACK=1。
- **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放运输连接。
- **窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。
## TCP 的三次握手
<div align="center"> <img src="../pics//086871db-5871-460f-97b7-126cd738bb0e.jpg" width=""/> </div><br>
假设 A 为客户端B 为服务器端。
1. 首先 B 处于 LISTEN监听状态等待客户的连接请求。
2. A 向 B 发送连接请求报文段SYN=1ACK=0选择一个初始的序号 x。
3. B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段SYN=1ACK=1确认号为 x+1同时也选择一个初始的序号 y。
4. A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1序号为 x+1。
5. B 收到 A 的确认后,连接建立。
## TCP 的四次挥手
<div align="center"> <img src="../pics//78f65456-666b-4044-b4ee-f7692dbbc0d3.jpg" width=""/> </div><br>
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK因为 ACK 在连接建立之后都为 1。
1. A 发送连接释放报文段FIN=1
2. B 收到之后发出确认,此时 TCP 属于半关闭状态B 能向 A 发送数据但是 A 不能向 B 发送数据;
3. 当 B 要不再需要连接时发送连接释放请求报文段FIN=1
4. A 收到后发出确认,此时连接释放。
### TIME_WAIT
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间。这么做有两个理由:
1. 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段那么就会重新发送连接释放请求报文段A 等待一段时间就是为了处理这种情况的发生。
2. 可能存在“已失效的连接请求报文段”,为了防止这种报文段出现在本次连接之外,需要等待一段时间。
## TCP 滑动窗口
<div align="center"> <img src="../pics//223fc26e-2fd6-484c-bcb7-443cac134f15.jpg" width=""/> </div><br>
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 32, 34, 35},其中 {31, 32} 按序到达,而 {34, 35} 就不是,因此只对字节 32 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
## TCP 可靠传输
TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT加权平均往返时间 RTTs 计算如下:
<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTTs=(1-a)*(RTTs)+a*RTT"/></div> <br>
超时时间 RTO 应该略大于 RTTsTCP 使用的超时时间计算如下:
<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTO=RTTs+4*RTT_d"/></div> <br>
其中 RTT<sub>d</sub> 为偏差。
## TCP 流量控制
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。例如将窗口字段设置为 0则发送方不能发送数据。
## TCP 拥塞控制
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接受,而拥塞控制是为了降低整个网络的拥塞程度。
<div align="center"> <img src="../pics//a69af9bb-b5ad-4896-862d-697e5ee4feb1.png" width=""/> </div><br>
TCP 主要通过四种算法来进行拥塞控制慢开始、拥塞避免、快重传、快恢复。发送方需要维护有一个叫做拥塞窗口cwnd的状态变量。注意拥塞窗口与发送方窗口的区别拥塞窗口只是一个状态变量实际决定发送方能发送多少数据的是发送方窗口。
为了便于讨论,做如下假设:
1. 接收方有足够大的接收缓存,因此不会发生流量控制;
2. 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。
<div align="center"> <img src="../pics//346244ff-98c1-4f12-9a87-d0832e8c04cf.jpg" width=""/> </div><br>
### 1. 慢开始与拥塞避免
发送的最初执行慢开始,令 cwnd=1发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍因此之后发送方能够发送的报文段为2、4、8 ...
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd / 2然后重新执行慢开始。
### 2. 快重传与快恢复
在接收方,要求每次接收到报文段都应该发送对已收到有序报文段的确认,例如已经接收到 M<sub>1</sub> 和 M<sub>2</sub>,此时收到 M<sub>4</sub>,应当发送对 M<sub>2</sub> 的确认。
在发送方,如果收到三个重复确认,那么可以确认下一个报文段丢失,例如收到三个 M<sub>2</sub> ,则 M<sub>3</sub> 丢失。此时执行快重传,立即重传下一个报文段。
在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd / 2 cwnd = ssthresh注意到此时直接进入拥塞避免。
<div align="center"> <img src="../pics//b18d679b-c8e2-4564-88ee-7600090e46da.jpg" width=""/> </div><br>
# 六、应用层*
## 域名系统 DNS
把主机名解析为 IP 地址。
被设计成分布式系统。
### 1. 层次结构
一个域名由多个层次构成,从上层到下层分别为顶级域名、二级域名、三级域名以及四级域名。所有域名可以画成一颗域名树。
<div align="center"> <img src="../pics//c2117f61-1177-4768-bf33-cf4f950d911c.png" width=""/> </div><br>
<div align="center"> <img src="../pics//a4b162e5-db2a-4a27-b213-1fe481c5a06a.png" width=""/> </div><br>
域名服务器可以分为以下四类:
1. 根域名服务器:解析顶级域名;
2. 顶级域名服务器:解析二级域名;
3. 权限域名服务器:解析区内的域名;
4. 本地域名服务器:也称为默认域名服务器。可以在其中配置高速缓存。
区和域的概念不同,可以在一个域中划分多个区。图 b 在域 abc.com 中划分了两个区abc.com 和 y.abc.com
<div align="center"> <img src="../pics//fc0c6b2d-68c7-4de8-aaaa-97355a4f0472.jpg" width=""/> </div><br>
因此就需要两个权限域名服务器:
<div align="center"> <img src="../pics//8b335d94-c1ca-42e1-ad48-bb179d28a4f1.jpg" width=""/> </div><br>
### 2. 解析过程
主机向本地域名服务器解析的过程采用递归,而本地域名服务器向其它域名服务器解析可以使用递归和迭代两种方式。
迭代的方式下,本地域名服务器向一个域名服务器解析请求解析之后,结果返回到本地域名服务器,然后本地域名服务器继续向其它域名服务器请求解析;而递归地方式下,结果不是直接返回的,而是继续向前请求解析,最后的结果才会返回。
<div align="center"> <img src="../pics//6bc61bb8-3b1c-4dc8-ac25-cef925ace0eb.jpg" width=""/> </div><br>
## 文件传输协议 FTP
FTP 在运输层使用 TCP并且需要建立两个并行的 TCP 连接:控制连接和数据连接。控制连接在整个会话期间一直保持打开,而数据连接在数据传送完毕之后就关闭。控制连接使用端口号 21数据连接使用端口号 20。
<div align="center"> <img src="../pics//58633775-8584-4a01-ad3f-eee4d9a466e1.jpg" width=""/> </div><br>
## 远程终端协议 TELNET
TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。
TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。
## 万维网 WWW
[HTTP](https://github.com/CyC2018/InterviewNotes/blob/master/notes/HTTP.md)
## 电子邮件协议
一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件发送协议和读取协议。其中发送协议常用 SMTP读取协议常用 POP3 和 IMAP。
<div align="center"> <img src="../pics//de1e46d2-748f-4da3-a29e-7de7bc840366.jpg" width=""/> </div><br>
### 1. POP3
POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。
### 2. IMAP
IMAP 协议中客户端和服务器上的邮件保持同步如果不去手动删除邮件那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。IMAP 协议也支持创建自定义的文件夹。
### 3. SMTP
SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP而是增加邮件主题的结构定义了非 ASCII 码的编码规则。
<div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png" width=""/> </div><br>
## 动态主机配置协议 DHCP
DHCP 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。
DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、默认路由器 IP 地址、域名服务器的 IP 地址。
工作方式如下:需要 IP 地址的主机广播发送 DHCP 发现报文(将目的地址置为全 1即 255.255.255.255:67源地址设置为全 0即 0.0.0.0:68DHCP 服务器收到发现报文之后,则在 IP 地址池中取一个地址,发送 DHCP 提供报文给该主机。
## 点对点传输 P2P
把某个文件分发的所有对等集合称为一个洪流。文件的数据单元称为文件块,它的大小是固定的。一个新的对等方加入某个洪流,一开始并没有文件块,但是能够从其它对等方中逐渐地下载到一些文件块,与此同时,它也为别的对等方上传一些文件块。
每个洪流都有一个基础设施,称为追踪器。当一个对等方加入洪流时,必须向追踪器登记,并周期性地通知追踪器它仍在洪流中。可以在任何时间加入和退出某个洪流。
一个新的对等方加入洪流时,追踪器会随机从洪流中选择若干个对等方,并让新对等方与这些对等方建立连接,把这些对等方称为相邻对等方。接收和发送文件块都是在相邻对等方中进行。
当一个对等方需要很多文件块时,通过使用最稀有优先的策略来取得文件块,也就是一个文件块在相邻对等方中副本最少,那么就优先请求这个文件块。
当很多对等方向同一个对等方请求文件块时,该对等方优先选择以最高速率向其发送文件块的对等方。
P2P 是一个分布式系统,任何时候都有对等方加入或者退出。使用分布式散列表 DHT可以查找洪流中的资源和 IP 地址映射。
## Web 页面请求过程
### 1. DHCP 配置主机信息
1. 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。
2. 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。
3. 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址0.0.0.0)的 IP 数据报中。
4. 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF将广播到与交换机连接的所有设备。
5. 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文该报文包含以下信息IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。
8. 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。
9. 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。
### 2. ARP 解析 MAC 地址
1. 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。
2. 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。
3. 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。
4. 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。
5. DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。
6. 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址FF:FF:FF:FF:FF:FF的以太网帧中并向交换机发送该以太网帧交换机将该帧转发给所有的连接设备包括网关路由器。
7. 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。
### 3. DNS 解析域名
1. 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。
2. 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。
3. 因为路由器具有内部网关协议RIP、OSPF和外部网关协议BGP这两种路由选择协议因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。
4. 到达 DNS 服务器之后DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。
5. 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。
### 4. HTTP 请求页面
1. 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。
2. 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。
3. HTTP 服务器收到该报文段之后,生成 TCP SYNACK 报文段,发回给主机。
4. 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。
5. HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。
6. 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。
## 常用端口
| 应用层协议 | 端口号 | 运输层协议 |
| -- | -- | -- |
| DNS | 53 | UDP |
| FTP | 控制连接 21数据连接 20 | TCP |
| TELNET | 23 | TCP |
| DHCP | 67 68 | UDP |
| HTTP | 80 | TCP |
| SMTP | 25 | TCP |
| POP3 | 110 | TCP |
| IMAP | 143 | TCP |
# 参考资料
- 计算机网络 第七版
- 计算机网络 自顶向下方法

1800
notes/设计模式.md Normal file

File diff suppressed because it is too large Load Diff

1252
notes/重构.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,285 +1,349 @@
[TOC]
# S.O.L.I.D
S.O.L.I.D 是面向对象设计和编程 (OOD&OOP) 中几个重要编码原则 (Programming Priciple) 的首字母缩写。
| 简写    | 全拼    | 中文翻译 |
| -- | -- | -- |
|SRP|    The Single Responsibility Principle    | 单一责任原则 |
|OCP|    The Open Closed Principle            | 开放封闭原则 |
|LSP|    The Liskov Substitution Principle    | 里氏替换原则 |
|ISP|    The Interface Segregation Principle    | 接口分离原则 |
|DIP|    The Dependency Inversion Principle    | 依赖倒置原则 |
<!-- GFM-TOC -->
* [一、设计原则](#一设计原则)
* [S.O.L.I.D](#solid)
* [其他常见原则](#其他常见原则)
* [二、三大特性](#二三大特性)
* [封装](#封装)
* [继承](#继承)
* [多态](#多态)
* [三、UML](#三uml)
* [类图](#类图)
* [时序图](#时序图)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 1. 单一责任原则
# 一、设计原则
当需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类
设计原则可以帮助我们避免那些糟糕的设计
## 2. 开放封闭原则
## S.O.L.I.D
软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
| 简写 | 全拼 | 中文翻译 |
| -- | -- | -- |
| SRP | The Single Responsibility Principle | 单一责任原则 |
| OCP | The Open Closed Principle | 开放封闭原则 |
| LSP | The Liskov Substitution Principle | 里氏替换原则 |
| ISP | The Interface Segregation Principle | 接口分离原则 |
| DIP | The Dependency Inversion Principle | 依赖倒置原则 |
## 3. 里氏替换原则
### 1. 单一责任原则
当一个子类的实例应该能够替换任何其超类的实例时它们之间才具有 is-a 关系。
> 修改一个类的原因应该只有一个
## 4. 接口分离原则
换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好
如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力
## 5. 依赖倒置原则
### 2. 开放封闭原则
1.  高层模块不应该依赖于低层模块,二者都应该依赖于抽象
2.  抽象不应该依赖于细节,细节应该依赖于抽象
> 类应该对扩展开放,对修改关闭。
# 封装、继承、多态
扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。
封装、继承、多态是面向对象的三大特性
符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码
## 1. 封装
### 3. 里氏替换原则
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。
> 子类对象必须能够替换掉所有父类对象。
继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。
### 4. 接口分离原则
> 不应该强迫客户依赖于它们不用的方法。
因此使用多个专门的接口比使用单一的总接口总要好。
### 5. 依赖倒置原则
> 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
> 抽象不应该依赖于细节,细节应该依赖于抽象。
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于底层模块,那么底层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
依赖于抽象意味着:
- 任何变量都不应该持有一个指向具体类的指针或者引用;
- 任何类都不应该从具体类派生;
- 任何方法都不应该覆写它的任何基类中的已经实现的方法。
## 其他常见原则
除了上述的经典原则,在实际开发中还有下面这些常见的设计原则。
| 简写 | 全拼 | 中文翻译 |
| -- | -- | -- |
|LOD| The Law of Demeter | 迪米特法则 |
|CRP| The Composite Reuse Principle | 合成复用原则 |
|CCP| The Common Closure Principle | 共同封闭原则 |
|SAP| The Stable Abstractions Principle | 稳定抽象原则 |
|SDP| The Stable Dependencies Principle | 稳定依赖原则 |
### 1. 迪米特法则
迪米特法则又叫作最少知道原则Least Knowledge Principle 简写LKP就是说一个对象应当对其他对象有尽可能少的了解不和陌生人说话。
### 2. 合成复用原则
尽量使用对象组合,而不是继承来达到复用的目的。
### 3. 共同封闭原则
一起修改的类,应该组合在一起(同一个包里)。如果必须修改应用程序里的代码,我们希望所有的修改都发生在一个包里(修改关闭),而不是遍布在很多包里。
### 4. 稳定抽象原则
最稳定的包应该是最抽象的包,不稳定的包应该是具体的包,即包的抽象程度跟它的稳定性成正比。
### 5. 稳定依赖原则
包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。
# 二、三大特性
## 封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
封装有三大好处:
1. 良好的封装能够减少耦合。
2. 类内部的结构可以自由修改。
3. 可以对成员进行更精确的控制。
4. 隐藏信息,实现细节。
1. 减少耦合
2. 隐藏内部细节,因此内部结构可以自由修改
3. 可以对成员进行更精确的控制
以下 Person 类封装 name、gender、age 等属性外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性而无法获取 age 属性但是 age 属性可以供 work() 方法使用。
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
注意到 gender 属性使用 int 数据类型进行存储封装使得用户注意不到这种实现细节。并且在需要修改使用的数据类型时也可以在不影响客户端代码的情况下进行。
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。
```java
public class Person {
    private String name;
    private int gender;
    private int age;
public class Person {
private String name;
private int gender;
private int age;
    public String getName() {
        return name;
    }
public String getName() {
return name;
}
    public String getGender() {
        return gender == 0 ? "man" : "woman";
    }
public String getGender() {
return gender == 0 ? "man" : "woman";
}
    public void work() {
        if(18 <= age && age <= 50) {
            System.out.println(name + " is working very hard!");
        } else {
            System.out.println(name + " can't work!");
        }
    }
public void work() {
if(18 <= age && age <= 50) {
System.out.println(name + " is working very hard!");
} else {
System.out.println(name + " can't work any more!");
}
}
}
```
## 2. 继承
## 继承
继承实现了 **is-a** 关系例如 Cat  Animal 就是一种 is-a 关系因此可以将 Cat 继承自 Animal从而获得 Animal  private 的属性和方法。
继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal从而获得 Animal 非 private 的属性和方法。
Cat 可以当做 Animal 来使用也就是可以使用 Animal 引用 Cat 对象这种子类转换为父类称为 **向上转型**
继承应该遵循里氏替换原则当一个子类的实例应该能够替换任何其超类的实例时它们之间才具有 is-a 关系。
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型**
```java
Animal animal = new Cat();
Animal animal = new Cat();
```
## 3. 多态
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
多态分为编译时多态和运行时多态。编译时多态主要指方法的重装,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。
## 多态
多态有三个条件1. 继承2. 覆盖父类方法3. 向上转型。
多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
下面的代码中乐器类Instrument有两个子类Wind  Percussion它们都覆盖了 play() 方法并且在 main() 方法中使用父类 Instrument 来引用 Wind  Percussion 对象。在 Instrument 引用调用 play() 方法时会执行实际引用对象所在类的 play() 方法而不是 Instrument 类的方法。
运行时多态有三个条件:
1. 继承
2. 覆盖
3. 向上转型
下面的代码中乐器类Instrument有两个子类Wind 和 Percussion它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。
```java
public class Instrument {
    public void play() {
        System.out.println("Instument is playing...");
    }
public class Instrument {
public void play() {
System.out.println("Instument is playing...");
}
}
public class Wind extends Instrument {
    public void play() {
        System.out.println("Wind is playing...");
    }
public class Wind extends Instrument {
public void play() {
System.out.println("Wind is playing...");
}
}
public class Percussion extends Instrument {
    public void play() {
        System.out.println("Percussion is playing...");
    }
public class Percussion extends Instrument {
public void play() {
System.out.println("Percussion is playing...");
}
}
public class Music {
    public static void main(String[] args) {
        List<Instrument> instruments = new ArrayList<>();
        instruments.add(new Wind());
        instruments.add(new Percussion());
        for(Instrument instrument : instruments) {
            instrument.play();
        }
    }
public class Music {
public static void main(String[] args) {
List<Instrument> instruments = new ArrayList<>();
instruments.add(new Wind());
instruments.add(new Percussion());
for(Instrument instrument : instruments) {
instrument.play();
}
}
}
```
# 三、UML
## 类图
# UML
### 1. 继承相关
## 1. 类图
继承有两种形式 : 泛化Generalize和实现Realize表现为 IS-A 关系。
**1.1 继承相关**
#### 泛化关系 (Generalize)
继承有两种形式 : 泛化generalize和实现realize表现为 is-a 关系
从具体类中继承。
① 泛化关系 (generalization)
<div align="center"> <img src="../pics//29badd92-109f-4f29-abb9-9857f5973928.png"/> </div><br>
从具体类中继承
#### 实现关系 (Realize)
![](index_files/29badd92-109f-4f29-abb9-9857f5973928.png)
从抽象类或者接口中继承。
② 实现关系 (realize)
<div align="center"> <img src="../pics//4b16e1d3-3a60-472c-9756-2f31b1c48abe.png"/> </div><br>
从抽象类或者接口中继承
### 2. 整体和部分
![](index_files/4b16e1d3-3a60-472c-9756-2f31b1c48abe.png)
#### 聚合关系 (Aggregation)
**1.2 整体和部分**
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。以下表示 B 由 A 组成:
① 聚合关系 (aggregation)
<div align="center"> <img src="../pics//34259bb8-ca3a-4872-8771-9e946782d9c3.png"/> </div><br>
表示整体由部分组成但是整体和部分不是强依赖的整体不存在了部分还是会存在。以下表示 B  A 组成
![](index_files/34259bb8-ca3a-4872-8771-9e946782d9c3.png)
② 组合关系 (composition)
#### 组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
![](index_files/7dda050d-ac35-4f47-9f51-18f18ed6fa9a.png)
<div align="center"> <img src="../pics//7dda050d-ac35-4f47-9f51-18f18ed6fa9a.png"/> </div><br>
**1.3 相互联系**
### 3. 相互联系
① 关联关系 (association)
#### 关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1  1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
![](index_files/4ccd294c-d6b2-421b-839e-d88336ff5fb7.png)
<div align="center"> <img src="../pics//4ccd294c-d6b2-421b-839e-d88336ff5fb7.png"/> </div><br>
② 依赖关系 (dependency)
#### 依赖关系 (Dependency)
和关联关系不同的是 , 依赖关系是在运行过程中起作用的。一般依赖作为类的构造器或者方法的参数传入。双向依赖时一种不好的设计。
和关联关系不同的是 , 依赖关系是在运行过程中起作用的。一般依赖作为类的构造器或者方法的参数传入。双向依赖时一种不好的设计。
![](index_files/47ca2614-509f-476e-98fc-50ec9f9d43c0.png)
<div align="center"> <img src="../pics//47ca2614-509f-476e-98fc-50ec9f9d43c0.png"/> </div><br>
## 2. 时序图
## 时序图
http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html
**2.1 定义**
### 1. 定义
时序图描述了对象之间传递消息的时间顺序,它用来表示用例的行为顺序。它的主要作用是通过对象间的交互来描述用例(注意是对象),从而寻找类的操作。
**2.2 赤壁之战时序图**
### 2. 赤壁之战时序图
从虚线从上往下表示时间的推进。
![](index_files/80c5aff8-fc46-4810-aeaa-215b5c60a003.png)
<div align="center"> <img src="../pics//80c5aff8-fc46-4810-aeaa-215b5c60a003.png"/> </div><br>
可见,通过时序图可以知道每个类具有以下操作:
```java
publc class 刘备 {
   public void 应战 ();
publc class 刘备 {
public void 应战 ();
}
publc class  孔明 {
  public void  拟定策略 ();
  public void  联合孙权 ();
  private void 借东风火攻 ();
publc class 孔明 {
public void 拟定策略 ();
public void 联合孙权 ();
private void 借东风火攻 ();
}
public class 关羽 {
    public void  防守荊州 ();
public class 关羽 {
public void 防守荊州 ();
}
public class 张飞 {
   public void  防守荆州前线 ();
public class 张飞 {
public void 防守荆州前线 ();
}
public class 孙权 {
   public void  领兵相助 ();
public class 孙权 {
public void 领兵相助 ();
}
```
**2.3 活动图、时序图之间的关系**
### 3. 活动图、时序图之间的关系
活动图示从用户的角度来描述用例;
时序图是从计算机的角度(对象间的交互)描述用例。
**2.4 类图与时序图的关系**
### 4. 类图与时序图的关系
类图描述系统的静态结构,时序图描述系统的动态行为。
**2.5 时序图的组成**
### 5. 时序图的组成
① 对象
#### 对象
有三种表现形式
![](index_files/25b8adad-2ef6-4f30-9012-c306b4e49897.png)
<div align="center"> <img src="../pics//25b8adad-2ef6-4f30-9012-c306b4e49897.png"/> </div><br>
在画图时,应该遵循以下原则:
1. 把交互频繁的对象尽可能地靠拢。
1. 把交互频繁的对象尽可能地靠拢。
2. 把初始化整个交互活动的对象(有时是一个参与者)放置在最左边。
2. 把初始化整个交互活动的对象(有时是一个参与者)放置在最左边。
② 生命线
#### 生命线
生命线从对象的创建开始到对象销毁时终止
![](index_files/b7b0eac6-e7ea-4fb6-8bfb-95fec6f235e2.png)
<div align="center"> <img src="../pics//b7b0eac6-e7ea-4fb6-8bfb-95fec6f235e2.png"/> </div><br>
③ 消息
#### 消息
对象之间的交互式通过发送消息来实现的。
消息有 4 种类型:
消息有 4 种类型:
1\. 简单消息,不区分同步异步。
1\. 简单消息,不区分同步异步。
![](index_files/a13b62da-0fa8-4224-a615-4cadacc08871.png)
<div align="center"> <img src="../pics//a13b62da-0fa8-4224-a615-4cadacc08871.png"/> </div><br>
2\. 同步消息,发送消息之后需要暂停活动来等待回应。
2\. 同步消息,发送消息之后需要暂停活动来等待回应。
![](index_files/33821037-dc40-4266-901c-e5b38e618426.png)
<div align="center"> <img src="../pics//33821037-dc40-4266-901c-e5b38e618426.png"/> </div><br>
3\. 异步消息,发送消息之后不需要等待。
3\. 异步消息,发送消息之后不需要等待。
![](index_files/dec6c6cc-1b5f-44ed-b8fd-464fcf849dac.png)
<div align="center"> <img src="../pics//dec6c6cc-1b5f-44ed-b8fd-464fcf849dac.png"/> </div><br>
4\. 返回消息,可选。
4\. 返回消息,可选。
④ 激活
#### 激活
生命线上的方框表示激活状态,其它时间处于休眠状态。
![](index_files/6ab5de9b-1c1e-4118-b2c3-fb6c7ed7de6f.png)
<div align="center"> <img src="../pics//6ab5de9b-1c1e-4118-b2c3-fb6c7ed7de6f.png"/> </div><br>
# 参考资料
# 参考资料
- Java 编程思想
- [ 面向对象设计的 SOLID 原则 ](http://www.cnblogs.com/shanyou/archive/2009/09/21/1570716.html)
- [ 看懂 UML 类图和时序图 ](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#generalization)
- [UML 系列——时序图顺序图sequence diagram](http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html)
- [ 面向对象编程三大特性 ------ 封装、继承、多态 ](http://blog.csdn.net/jianyuerensheng/article/details/51602015)
- Java 编程思想
- 敏捷软件开发:原则、模式与实践
- [面向对象设计的 SOLID 原则](http://www.cnblogs.com/shanyou/archive/2009/09/21/1570716.html)
- [看懂 UML 类图和时序图](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#generalization)
- [UML 系列——时序图顺序图sequence diagram](http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html)
- [面向对象编程三大特性 ------ 封装、继承、多态](http://blog.csdn.net/jianyuerensheng/article/details/51602015)

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB