text:
<repo_name>sx89/Java-notes<file_sep>/notes/Java-虚拟机.md

- [JDK,JRE,JVM,HotSpot关系](#jdkjrejvmhotspot关系)
    - [JIT的两个模式](#jit的两个模式)
    - [JIT动态编译的好处](#jit动态编译的好处)
- [内存泄漏与内存溢出](#内存泄漏与内存溢出)
    - [内存泄漏](#内存泄漏)
        - [定义](#定义)
        - [解决办法](#解决办法)
    - [内存溢出](#内存溢出)
- [运行时数据区域](#运行时数据区域)
    - [程序计数器](#程序计数器)
    - [Java 虚拟机栈](#java-虚拟机栈)
    - [本地方法栈](#本地方法栈)
    - [堆](#堆)
        - [java对象实例结构](#java对象实例结构)
        - [配置解释](#配置解释)
    - [方法区](#方法区)
    - [运行时常量池](#运行时常量池)
    - [直接内存](#直接内存)
    - [jvm分区的作用再理解](#jvm分区的作用再理解)
- [垃圾收集](#垃圾收集)
    - [判断一个对象是否可被回收](#判断一个对象是否可被回收)
        - [1. 引用计数算法](#1-引用计数算法)
        - [2. 可达性分析算法](#2-可达性分析算法)
        - [3. 方法区的回收](#3-方法区的回收)
        - [4. finalize()](#4-finalize)
    - [引用类型](#引用类型)
        - [1. 强引用](#1-强引用)
        - [2. 软引用](#2-软引用)
        - [3. 弱引用](#3-弱引用)
        - [4. 虚引用](#4-虚引用)
    - [垃圾收集算法](#垃圾收集算法)
        - [1. 标记 - 清除](#1-标记---清除)
        - [2. 标记 - 整理](#2-标记---整理)
        - [3. 复制](#3-复制)
        - [4. 分代收集](#4-分代收集)
    - [垃圾收集器](#垃圾收集器)
        - [1. Serial 收集器](#1-serial-收集器)
        - [2. ParNew 收集器](#2-parnew-收集器)
        - [3. Parallel Scavenge 收集器(平行清除)](#3-parallel-scavenge-收集器平行清除)
        - [4. Serial Old 收集器(以下是老年代收集器)](#4-serial-old-收集器以下是老年代收集器)
        - [5. Parallel Old 收集器](#5-parallel-old-收集器)
        - [6. CMS 收集器](#6-cms-收集器)
        - [7. G1 收集器](#7-g1-收集器)
- [三、内存分配与回收策略](#三内存分配与回收策略)
    - [Minor GC 和 Full GC](#minor-gc-和-full-gc)
    - [内存分配策略](#内存分配策略)
        - [1. 对象优先在 Eden 分配](#1-对象优先在-eden-分配)
        - [2. 大对象直接进入老年代](#2-大对象直接进入老年代)
        - [3. 长期存活的对象进入老年代](#3-长期存活的对象进入老年代)
        - [4. 动态对象年龄判定](#4-动态对象年龄判定)
        - [5. 空间分配担保(先看最大空间,再看平均空间)](#5-空间分配担保先看最大空间再看平均空间)
    - [Full GC 的触发条件](#full-gc-的触发条件)
        - [1. 调用 System.gc()](#1-调用-systemgc)
        - [2. 老年代空间不足](#2-老年代空间不足)
        - [3. 空间分配担保失败](#3-空间分配担保失败)
        - [4. JDK 1.7 及以前的永久代空间不足](#4-jdk-17-及以前的永久代空间不足)
        - [5. Concurrent Mode Failure](#5-concurrent-mode-failure)
- [JVM类生命周期概述：加载时机与加载过程](#jvm类生命周期概述加载时机与加载过程)
    - [类加载机制定义](#类加载机制定义)
    - [类的生命周期](#类的生命周期)
    - [加载时机与初始化时机](#加载时机与初始化时机)
        - [类加载时机](#类加载时机)
        - [类初始化时机(五种主动引用)](#类初始化时机五种主动引用)
        - [类的被动引用(不触发初始化)](#类的被动引用不触发初始化)
        - [实例化小结: 实例化就是对象实例变量,代码,构造器的初始化,字节码中是init函数(要与静态变量,代码的初始化区分开,字节码中是clinit函数)](#实例化小结-实例化就是对象实例变量代码构造器的初始化字节码中是init函数要与静态变量代码的初始化区分开字节码中是clinit函数)
    - [区分 准备阶段,初始化阶段,实例化阶段](#区分-准备阶段初始化阶段实例化阶段)
        - [常见考题](#常见考题)

# JDK,JRE,JVM,HotSpot关系
JDK是java开发环境,JRE是java运行环境,JDK包含JRE

JRE包含JVM,HotSpot是JVM的一种技术实现,用C和汇编语言编写,包含两个编译器(JIT 的server版本,client版本)和一个解释器

JVM包含解释器与编译器,用来运行字节码(.class文件,是特殊的二进制文件，二进制字节码文件),javac.exe可以简单看成是Java编译器。

## JIT的两个模式(*Just-in-time* compilation即时编译)
client模式是一种轻量级编译器，也叫C1编译器，占用内存小，启动快，但是执行效率没有server模式高，默认状态下不进行动态编译，适用于桌面应用程序。

server模式是一种重量级编译器，也叫C2编译器，启动慢，占用内存大，执行效率高，默认是开启动态编译的，适合服务器应用。

XX:RewriteFrequentPairs   用于开启动态编译。

-Xint:禁用JIT编译，即禁用两个编译器，纯解释执行。

-Xcomp:纯编译执行，如果方法无法编译，则回退到解释执行模式解释无法编译的代码。
## JIT动态编译的好处
编译分为静态编译与动态编译,静态编译是字节码在装入虚拟机之前就转换成本地代码.动态编译是装入虚拟机之后,根据运行热度,把热度高的字节码转换成本地代码

好处:

1. 静态编译器通常很难准确预知程序运行过程中究竟什么部分最需要优化。函数调用都是很浪费系统时间的，因为有许多进栈出栈操作。因此有一种优化办法，就是把原来的函数调用，通过编译器的编译，改成非函数调用，把函数代码直接嵌到调用处，变成顺序执行。
2. 面向对象的语言支持多态，静态编译无效确定程序调用哪个方法，因为多态是在程序运行中确定调用哪个方法。

# 内存泄漏与内存溢出
## 内存泄漏
### 定义
内存泄漏就是对象被分配的内存没有被回收; 长生命周期的对象有一个短生命周期的内存,会导致内存泄漏

例子有:容器的内存泄漏,各种连接的close忘记,单例模式引用了其他短声明周期对象
```java
1.	public class Simple {
2.
3.	    Object object;
4.
5.	    public void method1(){
6.	        object = new Object();
7.	    //...其他代码
8.	    }
9.	}

```
这里的object实例，其实我们期望它只作用于method1()方法中，且其他地方不会再用到它，但是，当method1()方法执行完成后，object对象所分配的内存不会马上被认为是可以被释放的对象，只有在Simple类创建的对象被释放后才会被释放，严格的说，这就是一种内存泄露。

### 解决办法
减小对象的作用域

及时对对象赋值为null

各种连接需要close也是这个道理,长周期对象短周期引用

## 内存溢出

# 一个类在内存里面的存储

Java运行时分以下区域：方法区、虚拟机栈、本地方法栈、堆、程序计数器

程序计数器：每条线程都需要一个独立的计数器

Java虚拟机栈：生命周期和线程相同，每个方法在执行的时候都会创建一个栈帧，用于存储局部变量表、操作数栈、动态数栈、方法出口等信息。每一个方法从调用直至执行完成的过程，就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

Java堆是被所有线程共享的一块内存区域，存放对象实例。

方法区：存储已被虚拟机加载的类信息，常量，静态变量。即时编译器编译后的代码数据。

# 运行时数据区域

虚拟机栈里面存放操作数栈,局部变量引用表,常量引用

## 程序计数器记录的是代码

当前线程所执行的字节码的行号指示器，为了线程切换后能够恢复到正确的执行位置，每条线程都需要有一个独立的程序计数器。

## **操作数栈**里面装的是数

**(Operand Stack)**也常称为操作栈，它是一个后入先出栈(LIFO)。同局部变量表一样，操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。

操作数栈的每一个元素可以是任意Java数据类型，32位的数据类型占一个栈容量，64位的数据类型占2个栈容量,且在方法执行的任意时刻，操作数栈的深度都不会超过max_stacks中设置的最大值。

当一个方法刚刚开始执行时，其操作数栈是空的，随着方法执行和字节码指令的执行，会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈，再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者，也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

## Java 虚拟机栈(装着栈帧)

每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈(存储着需要执行的指令)、常量池引用等信息。从方法调用直至执行完成的过程，对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小，在 JDK 1.4 中默认为 256K，而在 JDK 1.5+ 默认为 1M：

```java
java -Xss 2M HackTheJava
```

该区域可能抛出以下异常：

- 当线程请求的栈深度超过最大值，会抛出 StackOverflowError 异常；
- 栈进行动态扩展时如果无法申请到足够内存，会抛出 OutOfMemoryError 异常。

## 本地方法栈

本地方法栈与 Java 虚拟机栈类似，它们之间的区别只不过是本地方法栈为本地方法服务。

本地方法一般是用其它语言（C、C++ 或汇编语言等）编写的，并且被编译为基于本机硬件和操作系统的程序，对待这些方法需要特别处理。

## 堆

**所有对象实例都在这里分配内存，是垃圾收集的主要区域（"GC 堆"）。**

现代的垃圾收集器基本都是采用分代收集算法，其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块：

- 新生代（Young Generation）
- 老年代（Old Generation）

堆不需要连续内存，并且可以动态增加其内存，增加失败会抛出 OutOfMemoryError 异常。

可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小，第一个参数设置初始值，第二个参数设置最大值。

```java
java -Xms 1M -Xmx 2M HackTheJava
```
### java对象实例结构
在HotSpot虚拟机中，对象在内存中存储的布局可以分为3块区域：对象头（Header）、实例数据（Instance Data）和对齐填充（Padding）。下图是普通对象实例与数组对象实例的数据结构：

对象头

HotSpot虚拟机的对象头包括两部分信息：

markword

第一部分markword,用于存储对象自身的运行时数据，如哈希码（HashCode）、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等，这部分数据的长度在32位和64位的虚拟机（未开启压缩指针）中分别为32bit和64bit，官方称它为“MarkWord”。
klass
对象头的另外一部分是klass类型指针，即对象指向它的类元数据的指针，虚拟机通过这个指针来确定这个对象是哪个类的实例.
数组长度（只有数组对象有）
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
实例数据
实例数据部分是对象真正存储的有效信息，也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的，还是在子类中定义的，都需要记录起来。

对齐填充

第三部分对齐填充并不是必然存在的，也没有特别的含义，它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍，换句话说，就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数（1倍或者2倍），因此，当对象实例数据部分没有对齐时，就需要通过对齐填充来补全。

### 配置解释
```
先说VM选项， 三种：
- : 标准VM选项，VM规范的选项
- -X: 非标准VM选项，不保证所有VM支持
- -XX: 高级选项，高级特性，但属于不稳定的选项参见

其语义分别是：
-Xmx: 堆的最大内存数，等同于-XX:MaxHeapSize-Xms: 堆的初始化初始化大小
-Xmn: 堆中新生代初始及最大大小，如果需要进一步细化，初始化大小用
-XX:NewSize，最大大小用-XX:MaxNewSize -Xss: 线程栈大小，等同于
-XX:ThreadStackSize命名应该非简称，

助记的话： memory maximum, memory startup, memory nursery/new, stack size.

作者：Home3k
链接：https://www.zhihu.com/question/59957834/answer/170775050
来源：知乎
著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。
```
## 方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

**方法区是一个 JVM 规范，永久代与元空间都是其一种实现方式。在 JDK 1.8 之后，原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息，静态变量和常量池等放入堆中。**

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

和堆一样不需要连续的内存，并且可以动态扩展，动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载，但是一般比较难实现。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小，因为它受到很多因素影响，并且每次 Full GC 之后永久代的大小都会改变，所以经常会抛出 OutOfMemoryError 异常。**为了更容易管理方法区，从 JDK 1.8 开始，移除永久代，并把方法区移至元空间，它位于本地内存中，而不是虚拟机内存中。**

方法区是一个 JVM 规范，永久代与元空间都是其一种实现方式。在 JDK 1.8 之后，原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息，静态变量和常量池等放入堆中。

## 运行时常量池

运行时常量池是方法区的一部分。

Class 文件中的常量池（编译器生成的字面量和符号引用）会在类加载后被放入这个区域。

除了在编译期生成的常量，还允许动态生成，例如 String 类的 intern()。

## 直接内存

在 JDK 1.4 中新引入了 NIO 类，它可以使用 Native 函数库直接分配堆外内存，然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能，因为避免了在堆内存和堆外内存来回拷贝数据。

## jvm分区的作用再理解
方法区：类信息、类变量（静态变量和常量）、方法 (JDK1.8之后,静态变量和常量进入堆,类信息进入元空间)

堆：对象、成员变量

栈：局部变量 程序计数器 虚拟机栈(操作数栈,局部变量表,常量引用) native方法栈

（1）当程序运行时，首先通过类装载器加载字节码文件，经过解析后装入方法区！在方法区中存了类的各种信息，包括类变量、常量及方法。对于同一个方法的调用，同一个类的不同实例调用的都是存在方法区的同一个方法。类变量的生命周期从程序开始运行时创建，到程序终止运行时结束！

（2）当程序中new一个对象时，这个对象存在堆中，对象的成员变量都存在堆中，当对象被回收时，对象的成员变量随之消失！

（3）当方法调用时，JVM会在栈中分配一个栈桢，存储方法的局部变量。当方法调用结束时，局部变量消失！

类变量：属于类的属性信息，与类的实例无关，多个实例共用同一个类变量，存在与方法区中。类变量用static修饰，包括静态变量和常量。静态变量有默认初始值，常量必须声明同时初始化。

成员变量：属于实例的变量，只与实例有关，写在类下面，方法外，非static修饰。成员变量会随着成员的创建而生存，随着成员的回收而销毁。

局部变量：声明在方法中，没有默认初始值，随着方法的调用而创建，存储于栈中，随着方法调用的结束而销毁。

类的成员变量在不同对象中各不相同，基本数据类型和引用数据类型都存储在这个对象中，作为一个整体存储在堆中。而类的方法是所有的对象共享的，方法是存在方法区的，只用当调用的时候才会被压栈，不用的时候是占内存的。

## stackoverflowerror与outofmemoryerror区别

- **1、stackoverflow：**

    每当java程序启动一个新的线程时，java虚拟机会为他分配一个栈，java栈以帧为单位保持线程运行状态；当线程调用一个方法是，jvm压入一个新的栈帧到这个线程的栈中，只要这个方法还没返回，这个栈帧就存在。
    如果方法的嵌套调用层次太多(如递归调用),随着java栈中的帧的增多，最终导致这个线程的栈中的所有栈帧的大小的总和大于-Xss设置的值，而产生生StackOverflowError溢出异常。

- **2、outofmemory：**

- 2.1、栈内存溢出

    java程序启动一个新线程时，没有足够的空间为改线程分配java栈，一个线程java栈的大小由-Xss设置决定；JVM则抛出OutOfMemoryError异常。

- 2.2、堆内存溢出

    java堆用于存放对象的实例，当需要为对象的实例分配内存时，而堆的占用已经达到了设置的最大值(通过-Xmx)设置最大值，则抛出OutOfMemoryError异常。

- 2.3、方法区内存溢出

    方法区用于存放java类的相关信息，如类名、访问修饰符、常量池、字段描述、方法描述等。在类加载器加载class文件到内存中的时候，JVM会提取其中的类信息，并将这些类信息放到方法区中。
    当需要存储这些类信息，而方法区的内存占用又已经达到最大值（通过-XX:MaxPermSize）；将会抛出OutOfMemoryError异常对于这种情况的测试，基本的思路是运行时产生大量的类去填满方法区，直到溢出。这里需要借助CGLib直接操作字节码运行时，生成了大量的动态类。

    ## OOM的分类

    ## OOM for Heap  (java.lang.OutOfMemoryError: Java heap space)

    分析
    此OOM是由于JVM中heap的最大值不满足需要，将设置heap的最大值调高即可，参数样例为：-Xmx2G

    JVM堆的设置是指java程序运行过程中JVM可以调配使用的内存空间的设置.JVM在启动的时候会自动设置Heap size的值，其初始空间(即-Xms)是物理内存的1/64，最大空间(-Xmx)是物理内存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap size 的大小是Young Generation 和Tenured Generaion 之和。
    提示：在JVM中如果98％的时间是用于GC且可用的Heap size 不足2％的时候将抛出此异常信息。
    提示：Heap Size 最大不要超过可用物理内存的80％，一般的要将-Xms和-Xmx选项设置为相同，而-Xmn为1/4的-Xmx值。

    解决参考
    调高heap的最大值，即-Xmx的值调大。

    ## OOM for StackOverflowError  (Exception in thread "main" java.lang.StackOverflowError)

    分析

    如果线程请求的栈深度大于虚拟机所允许的最大深度，将抛出StackOverflowError异常。

    如果虚拟机在扩展栈时无法申请到足够的内存空间，则抛出OutOfMemoryError异常。

    一般在单线程程序情况下无法产生OutOfMemoryError异常，使用多线程方式也会出现OutOfMemeoryError，因为栈是线程私有的，线程多也会方法区溢出。

    解决参考
    检查程序是否有深度递归。

    ## OOM for Perm  (java.lang.OutOfMemoryError: PermGen space)

    分析
    PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理，所以如果你的应用中有很多CLASS的话,就很可能出现PermGen space错误,这种错误常见在web服务器对JSP进行pre compile的时候。如果你的WEB APP下都用了大量的第三方jar, 其大小超过了jvm默认的大小那么就会产生此错误信息了。由于JVM在默认的情况下，Perm默认为64M，而很多程序需要大量的Perm区内 存，尤其使用到像Spring等框架的时候，由于需要使用到动态生成类，而这些类不能被GC自动释放，所以导致OutOfMemoryError: PermGen space异常。解决方法很简单，增大JVM的 -XX:MaxPermSize 启动参数，就可以解决这个问题，如过使用的是默认变量通常是64M，改成128M就可以了，-XX:MaxPermSize=128m。如果已经是128m，就改成 256m。我一般在服务器上为安全起见，改成256m。

    解决参考
    调高Perm的最大值，即-XX:MaxPermSize的值调大。
    另外，注意一点，Perm一般是在JVM启动时加载类进来，如果是JVM运行较长一段时间而不是刚启动后溢出的话，很有可能是由于运行时有类被动态加载进来，此时建议用CMS策略中的类卸载配置。
    如：-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled

    ## OOM for GC  (java.lang.OutOfMemoryError: GC overhead limit exceeded)

    分析
    此OOM是由于JVM在GC时，对象过多，导致内存溢出，建议调整GC的策略，在一定比例下开始GC而不要使用默认的策略，或者将新代和老代设置合适的大小，需要进行微调存活率。

    解决参考
    改变GC策略，在老代80%时就是开始GC，并且将-XX:SurvivorRatio（-XX:SurvivorRatio=8）和-XX:NewRatio（-XX:NewRatio=4）设置的更合理。

    ## OOM for native thread created  (java.lang.OutOfMemoryError: unable to create new native thread)

    分析
    首先我们通过下面这个测试程序来认识这个问题：
    运行的环境 （有必要说明一下，不同环境会有不同的结果）：32位 Windows XP，Sun JDK 1.6.0_18， eclipse 3.4
    测试程序：

    import java.util.concurrent.CountDownLatch;

    public class TestNativeOutOfMemoryError {

        public static void main(String[] args) {

            for (int i = 0;; i++) {

                System.out.println("i = " + i);

                new Thread(new HoldThread()).start();

            }

        }

    }

    class HoldThread extends Thread {

        CountDownLatch cdl = new CountDownLatch(1);

        public HoldThread() {

            this.setDaemon(true);

        }

        public void run() {

            try {

                cdl.await();

            } catch (InterruptedException e) {

            }

        }

    }

    不指定任何JVM参数，eclipse中直接运行输出，输出：

    i = 5602
    Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
        at java.lang.Thread.start0(Native Method)
        at java.lang.Thread.start(Thread.java:597)
        at TestNativeOutOfMemoryError.main(TestNativeOutOfMemoryError.java:20)

    这个异常问题本质原因是我们创建了太多的线程，而能创建的线程数是有限制的，导致了异常的发生。能创建的线程数的具体计算公式如下：
    (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
    MaxProcessMemory 指的是一个进程的最大内存
    JVMMemory             JVM内存
    ReservedOsMemory  保留的操作系统内存
    ThreadStackSize      线程栈的大小

    在java语言里， 当你创建一个线程的时候，虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程，而这个系统线程的内存用的不是JVMMemory，而是系统中剩下的内存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。

    结合公式和例子进行说明：
    MaxProcessMemory 在32位的 windows下是 2G
    JVMMemory   eclipse默认启动的程序内存是64M
    ReservedOsMemory  一般是130M左右
    ThreadStackSize 32位  JDK 1.6默认的stacksize 325K左右
    公式如下：
    (2*1024*1024-64*1024-130*1024)/325 = 5841
    公式计算所得5841，和实践5602基本一致（有偏差是因为ReservedOsMemory不能很精确）

    依旧使用上面的测试程序，加上下面的JVM参数，测试结果如下：

    ThreadStackSize              JVMMemory              能创建的线程数
    默认的325K             -Xms1024m -Xmx1024m       i = 2655
    默认的325K             -Xms1224m -Xmx1224m       i = 2072
    默认的325K             -Xms1324m -Xmx1324m       i = 1753
    默认的325K             -Xms1424m -Xmx1424m       i = 1435
    -Xss1024k               -Xms1424m -Xmx1424m       i = 452

    解决参考
    如果JVM内存调的过大或者可利用率小于20%，可以建议将heap及perm的最大值下调，并将线程栈调小，即-Xss调小，如：-Xss128k。

    在JVM内存不能调小的前提下，将-Xss设置较小，如：-Xss:128k。

    ## OOM for allocate huge array  (Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit)

    分析
    此类信息表明应用程序试图分配一个大于堆大小的数组。例如，如果应用程序new一个数组对象，大小为512M，但是最大堆大小为256M，因此OutOfMemoryError会抛出，因为数组的大小超过虚拟机的限制。

    解决参考
    （1）、首先检查heap的-Xmx是不是设置的过小
    （2）、如果heap的-Xmx已经足够大，那么请检查应用程序是不是存在bug，例如：应用程序可能在计算数组的大小时，存在算法错误，导致数组的size很大，从而导致巨大的数组被分配。

    ## OOM for small swap (Exception in thread "main": java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space? )

    分析
     抛出这类错误，是由于从native堆中分配内存失败，并且堆内存可能接近耗尽。这类错误可能跟应用程序没有关系，例如下面两种原因也会导致错误的发生：
    （1）操作系统配置了较小的交换区
    （2）系统的另外一个进程正在消耗所有的内存

    解决参考
    （1）、检查os的swap是不是没有设置或者设置的过小
    （2）、检查是否有其他进程在消耗大量的内存，从而导致当前的JVM内存不够分配。
    注意：虽然有时<reason>部分显示导致OOM的原因，但大多数情况下，<reason>显示的是提示分配失败的源模块的名称，所以有必要查看日志文件，如crash时的hs文件。

    ## OOM for exhausted native memory  (java.lang.OutOfMemoryErr java.io.FileInputStream.readBytes(Native Method))

    分析
    从错误日志来看，在OOM后面没有提示引起OOM的原因，进一步查看stack trace发现，导致OOM的原因是由Native Method的调用引起的，另外检查Java heap，发现heap的使用正常，因而需要考虑问题的发生是由于Native memory被耗尽导致的。

    解决参考
    从根本上来说，解决此问题的方法应该通过检测发生问题时的环境下，native memory为什么被占用或者说为什么native memory越来越小，从而去解决引起Native memory减小的问题。但是如果此问题不容易分析时，可以通过以下方法或者结合起来处理。
    （1）、cpu和os保证是64位的，并且jdk也换为64位的。
    （2）、将java heap的-Xmx尽量调小，但是保证在不影响应用使用的前提下。
    （3）、限制对native memory的消耗，比如：将thread的-Xss调小，并且限制产生大量的线程；限制文件的io操作次数和数量；限制网络的使用等等。

# 垃圾收集

垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的，只存在于线程的生命周期内，线程结束之后就会消失，因此不需要对这三个区域进行垃圾回收。

## 判断一个对象是否可被回收

### 1. 引用计数算法

为对象添加一个引用计数器，当对象增加一个引用时计数器加 1，引用失效时计数器减 1。引用计数为 0 的对象可被回收。

在两个对象出现循环引用的情况下，此时引用计数器永远不为 0，导致无法对它们进行回收。正是因为循环引用的存在，因此 Java 虚拟机不使用引用计数算法。

```java
public class Test {

    public Object instance = null;

    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
        doSomething();
    }
}
```

在上述代码中，a 与 b 引用的对象实例互相持有了对象的引用，因此当我们把对 a 对象与 b 对象的引用去除之后，由于两个对象还存在互相之间的引用，导致两个 Test 对象无法被回收。

### 2. 可达性分析算法

以 GC Roots 为起始点进行搜索，可达的对象都是存活的，不可达的对象可被回收。

Java 虚拟机使用该算法来判断对象是否可被回收，GC Roots 一般包含以下内容：

- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI(Native方法) 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象

以下几种对象可以作为GC Root：

虚拟机栈中的引用对象

方法区中类静态属性引用的对象

方法区中常量引用对象

本地方法栈中JNI引用对象

### 3. 方法区的回收

因为方法区主要存放永久代对象，而永久代对象的回收率比新生代低很多，所以在方法区上进行回收性价比不高。

方法区的垃圾回收主要有两种，分别是对废弃常量的回收和对无用类的回收。

当一个常量对象不再任何地方被引用的时候，则被标记为废弃常量，这个常量可以被回收。

方法区中的类需要同时满足以下三个条件才能被标记为无用的类：

1.Java堆中不存在该类的任何实例对象；

2.加载该类的类加载器已经被回收；

3.该类对应的java.lang.Class对象不在任何地方被引用，且无法在任何地方通过反射访问该类的方法。

主要是对常量池的回收和对类的卸载。

为了避免内存溢出，在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。

类的卸载条件很多，需要满足以下三个条件，并且满足了条件也不一定会被卸载：

- 该类所有的实例都已经被回收，此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用，也就无法在任何地方通过反射访问该类方法。

### 4. finalize()

类似 C++ 的析构函数，用于关闭外部资源。但是 try-finally 等方式可以做得更好，并且该方法运行代价很高，不确定性大，无法保证各个对象的调用顺序，因此最好不要使用。

当一个对象可被回收时，如果需要执行该对象的 finalize() 方法，那么就有可能在该方法中让对象重新被引用，从而实现自救。自救只能进行一次，如果回收的对象之前调用了 finalize() 方法自救，后面回收时不会再调用该方法。

## 引用类型

无论是通过引用计数算法判断对象的引用数量，还是通过可达性分析算法判断对象是否可达，判定对象是否可被回收都与引用有关。

Java 提供了四种强度不同的引用类型。

### 1. 强引用

被强引用关联的对象不会被回收。

使用 new 一个新对象的方式来创建强引用。

```java
Object obj = new Object();
```

### 2. 软引用

被软引用关联的对象只有在内存不够的情况下才会被回收。

使用 SoftReference 类来创建软引用。

```java
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联
```

### 3. 弱引用

被弱引用关联的对象一定会被回收，也就是说它只能存活到下一次垃圾回收发生之前。

使用 WeakReference 类来创建弱引用。

```java
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
```

### 4. 虚引用

又称为幽灵引用或者幻影引用，一个对象是否有虚引用的存在，不会对其生存时间造成影响，也无法通过虚引用得到一个对象。

为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

使用 PhantomReference 来创建虚引用。

```java
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
```

## 垃圾收集算法

### 1. 标记 - 清除

在标记阶段，程序会检查每个对象是否为活动对象，如果是活动对象，则程序会在对象头部打上标记。

在清除阶段，会进行对象回收并取消标志位，另外，还会判断回收后的分块与前一个空闲分块是否连续，若连续，会合并这两个分块。回收对象就是把对象作为分块，连接到被称为 “空闲链表” 的单向链表，之后进行分配时只需要遍历这个空闲链表，就可以找到分块。

在分配时，程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size，会直接返回这个分块；如果找到的块大于 size，会将块分割成大小为 size 与 (block - size) 的两部分，返回大小为 size 的分块，并把大小为 (block - size) 的块返回给空闲链表。

不足：

- 标记和清除过程效率都不高；
- 会产生大量不连续的内存碎片，导致无法给大对象分配内存。

### 2. 标记 - 整理

让所有存活的对象都向一端移动，然后直接清理掉端边界以外的内存。

优点:

- 不会产生内存碎片

不足:

- 需要移动大量对象，处理效率比较低。

### 3. 复制

将内存划分为大小相等的两块，每次只使用其中一块，当这一块内存用完了就将还存活的对象复制到另一块上面，然后再把使用过的内存空间进行一次清理。

主要不足是只使用了内存的一半。

现在的商业虚拟机都采用这种收集算法回收新生代，但是并不是划分为大小相等的两块，而是一块较大的 Eden 空间和两块较小的 Survivor 空间，每次使用 Eden 和其中一块 Survivor。在回收时，将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上，最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1，保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活，那么一块 Survivor 就不够用了，此时需要依赖于老年代进行空间分配担保，也就是借用老年代的空间存储放不下的对象。

### 4. 分代收集

现在的商业虚拟机采用分代收集算法，它根据对象存活周期将内存划分为几块，不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

- 新生代使用：复制算法
- 老年代使用：标记 - 清除 或者 标记 - 整理 算法

## 垃圾收集器

以上是 HotSpot 虚拟机中的 7 个垃圾收集器，连线表示垃圾收集器可以配合使用。

- 单线程与多线程：单线程指的是垃圾收集器只使用一个线程，而多线程使用多个线程；
- 串行与并行：串行指的是垃圾收集器与用户程序交替执行，这意味着在执行垃圾收集的时候需要停顿用户程序；并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外，其它垃圾收集器都是以串行的方式执行。

### 1. Serial 收集器

Serial 翻译为串行，也就是说它以串行的方式执行。

它是单线程的收集器，只会使用一个线程进行垃圾收集工作。

它的优点是简单高效，在单个 CPU 环境下，由于没有线程交互的开销，因此拥有最高的单线程收集效率。

它是 Client 场景下的默认新生代收集器，因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内，只要不是太频繁，这点停顿时间是可以接受的。

### 2. ParNew 收集器

它是 Serial 收集器的多线程版本。多cpu下效率比serial好,但单核不如,因为有进程切换开销

它是 Server 场景下默认的新生代收集器，除了性能原因外，主要是因为除了 Serial 收集器，只有它能与 CMS 收集器配合使用。

### 3. Parallel Scavenge 收集器(平行清除)

与 ParNew 一样是多线程收集器。(适合交互少,吞吐量大的场景)

其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间，而它的目标是达到一个可控制的吞吐量，因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。

停顿时间越短就越适合需要与用户交互的程序，良好的响应速度能提升用户体验。

而高吞吐量则可以高效率地利用 CPU 时间，尽快完成程序的运算任务，适合在后台运算而不需要太多交互的任务。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的：**新生代空间变小，垃圾回收变得频繁，导致吞吐量下降。**

可以通过一个开关参数打开 GC 自适应的调节策略（GC Ergonomics），就不需要手工指定新生代的大小（-Xmn）、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息，动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

**新生代收集器分为(单线程serial,多线程ParNew,少交互多吞吐Parallel Scavenge)**

### 4. Serial Old 收集器(以下是老年代收集器)

是 Serial 收集器的老年代版本，也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下，它有两大用途：

- 在 JDK 1.5 以及之前版本（Parallel Old 诞生以前）中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案，在并发收集发生 Concurrent Mode Failure 时使用(并发失败,就用串行)

### 5. Parallel Old 收集器

是 Parallel Scavenge 收集器的老年代版本。

在注重吞吐量以及 CPU 资源敏感的场合，都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

### 6. CMS 收集器

CMS（Concurrent Mark Sweep），Mark Sweep 指的是标记 - 清除算法。

分为以下四个流程：

- 初始标记：仅仅只是标记一下 GC Roots 能直接关联到的对象，速度很快，需要停顿。
- 并发标记：进行 GC Roots Tracing 的过程，它在整个回收过程中耗时最长，不需要停顿。
- 重新标记：为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录，需要停顿。
- 并发清除：不需要停顿,耗时较长

在整个过程中耗时最长的并发标记和并发清除过程中，收集器线程都可以与用户线程一起工作，不需要进行停顿。初始标记和重新标记耗时短,要停顿.

具有以下缺点：

- 吞吐量低：低停顿时间是以牺牲吞吐量为代价的，导致 CPU 利用率不够高。
- 无法处理浮动垃圾，可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾，这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在，因此需要预留出一部分内存，意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾，就会出现 Concurrent Mode Failure，这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片，往往出现老年代空间剩余，但无法找到足够大连续空间来分配当前对象，不得不提前触发一次 Full GC。

### 7. G1 收集器

G1（Garbage-First），它是一款面向服务端应用的垃圾收集器，在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

堆被分为新生代和老年代，其它收集器进行收集的范围都是整个新生代或者老年代，而 G1 可以直接对新生代和老年代一起回收。

G1 把堆划分成多个大小相等的独立区域（Region），新生代和老年代不再物理隔离。

通过引入 Region 的概念，从而将原来的一整块内存空间划分成多个的小空间，使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性，使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间（这两个值是通过过去回收的经验获得），并维护一个优先列表，每次根据允许的收集时间，优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set，用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set，在做可达性分析的时候就可以避免全堆扫描。

如果不计算维护 Remembered Set 的操作，G1 收集器的运作大致可划分为以下几个步骤：

- 初始标记
- 并发标记
- 最终标记：为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录，虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面，最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程，但是可并行执行。
- 筛选回收：首先对各个 Region 中的回收价值和成本进行排序，根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行，但是因为只回收一部分 Region，时间是用户可控制的，而且停顿用户线程将大幅度提高收集效率。

具备如下特点：

- 空间整合：整体来看是基于“标记 - 整理”算法实现的收集器，从局部（两个 Region 之间）上来看是基于“复制”算法实现的，这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿：能让使用者明确指定在一个长度为 M 毫秒的时间片段内，消耗在 GC 上的时间不得超过 N 毫秒。
- 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势，使用多个CPU来缩短“Stop The World”停顿时间，部分其他收集器原本需要停顿Java线程执行的GC动作，G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集 与其他收集器一样，分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆，但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。

# 三、内存分配与回收策略

## Minor GC 和 Full GC

- Minor GC：回收新生代，因为新生代对象存活时间很短，因此 Minor GC 会频繁执行，执行的速度一般也会比较快。

- Full GC：回收老年代和新生代，老年代对象其存活时间长，因此 Full GC 很少执行，执行速度会比 Minor GC 慢很多。

## 内存分配策略

### 1. 对象优先在 Eden 分配

大多数情况下，对象在新生代 Eden 上分配，当 Eden 空间不够时，发起 Minor GC。

### 2. 大对象直接进入老年代

大对象是指需要连续内存空间的对象，最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold，大于此值的对象直接在老年代分配，避免在 Eden 和 Survivor 之间的大量内存复制。

### 3. 长期存活的对象进入老年代

为对象定义年龄计数器，对象在 Eden 出生并经过 Minor GC 依然存活，将移动到 Survivor 中，年龄就增加 1 岁，增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

### 4. 动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代，如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半，则年龄大于或等于该年龄的对象可以直接进入老年代，无需等到 MaxTenuringThreshold 中要求的年龄。

### 5. 空间分配担保(先看最大空间,再看平均空间)

在发生 Minor GC 之前，虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间，如果条件成立的话，那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败，如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小，如果大于，将尝试着进行一次 Minor GC；如果小于，或者 HandlePromotionFailure 的值不允许冒险，那么就要进行一次 Full GC。

## Full GC 的触发条件

对于 Minor GC，其触发条件非常简单，当 Eden 空间满时，就将触发一次 Minor GC。而 Full GC 则相对复杂，有以下条件：

### 1. 调用 System.gc()

只是建议虚拟机执行 Full GC，但是虚拟机不一定真正去执行。不建议使用这种方式，而是让虚拟机管理内存。

### 2. 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC，应当尽量不要创建过大的对象以及数组。除此之外，可以通过 -Xmn 虚拟机参数调大新生代的大小，让对象尽量在新生代被回收掉，不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄，让对象在新生代多存活一段时间。

### 3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保，如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。

### 4. JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前，HotSpot 虚拟机中的方法区是用永久代实现的，永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时，永久代可能会被占满，在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了，那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC，可采用的方法为增大永久代空间或转为使用 CMS GC。

### 5. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代，而此时老年代空间不足（可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足），便会报 Concurrent Mode Failure 错误，并触发 Full GC。

# JVM类生命周期概述：加载时机与加载过程
原文链接：https://blog.csdn.net/justloveyou_/article/details/72466105
## 类加载机制定义
一个.java文件在编译后会形成相应的一个或多个Class文件，这些Class文件中描述了类的各种信息，并且它们最终都需要被加载到虚拟机中才能被运行和使用。

虚拟机把描述类的数据从Class文件**加载到内存**，并对数据进行校验，转换解析和**初始化**，最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的**类加载机制**。

JVM类加载机制主要包括两个问题：类加载的时机与步骤 和 类加载的方式。

## 类的生命周期

包括以下 7 个阶段：

-  **加载（Loading）**
-  **验证（Verification）**
-  **准备（Preparation）**
-  **解析（Resolution）**
-  **初始化（Initialization）**
- 使用（Using）
- 卸载（Unloading）

Java类从被加载到虚拟机内存中开始，到卸载出内存为止，它的整个生命周期包括：加载（Loading）、验证（Verification）、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。其中准备、验证、解析3个部分统称为连接（Linking）

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的，类的加载过程必须按照这种顺序按部就班地开始，而解析阶段则不一定：它在某些情况下可以在初始化阶段之后再开始，这是为了支持Java语言的运行时绑定（也称为动态绑定或晚期绑定）。

这些阶段通常都是相互交叉地混合式进行的，也就是说通常会在一个阶段执行的过程中调用或激活另外一个阶段。

## 加载时机与初始化时机
### 类加载时机

什么情况下虚拟机需要开始加载一个类呢？虚拟机规范中并没有对此进行强制约束，这点可以交给虚拟机的具体实现来自由把握。程序运行期间虚拟机自行加载.

### 类初始化时机(五种主动引用)
**虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化（而这一过程自然发生在加载、验证、准备之后）：**

1) 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时，如果类没有进行过初始化，则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是：

使用new关键字实例化对象的时候；

读取或设置一个类的静态字段（被final修饰，已在编译器把结果放入常量池的静态字段除外）的时候；

调用一个类的静态方法的时候。

（注意，newarray指令触发的只是数组类型本身的初始化，而不会导致其相关类型的初始化，比如，new String[]只会直接触发String[]类的初始化，也就是触发对类[Ljava.lang.String的初始化，而直接不会触发String类的初始化）

2) 使用java.lang.reflect包的方法对类进行反射调用的时候，如果类没有进行过初始化，则需要先触发其初始化。

3) 当初始化一个类的时候，如果发现其父类还没有进行过初始化，则需要先触发其父类的初始化。

4) 当虚拟机启动时，用户需要指定一个要执行的主类（包含main()方法的那个类），虚拟机会先初始化这个主类。

5) 当使用jdk1.7动态语言支持时，如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄，并且这个方法句柄所对应的类没有进行初始化，则需要先出触发其初始化。

注意，对于这五种会触发类进行初始化的场景，虚拟机规范中使用了一个很强烈的限定语：“有且只有”，这五种场景中的行为称为对一个类进行** 主动引用**。除此之外，所有引用类的方式，都不会触发初始化，称为 **被动引用**。

特别需要指出的是，类的实例化与类的初始化是两个完全不同的概念：
类的实例化是指创建一个类的实例(对象)的过程；
类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程，是类生命周期中的一个阶段。
### 类的被动引用(不触发初始化)
1)、通过子类引用父类的静态字段，不会导致子类初始化
```java
public class SSClass{
    static{
        System.out.println("SSClass");
    }
}

public class SClass extends SSClass{
    static{
        System.out.println("SClass init!");
    }

    public static int value = 123;

    public SClass(){
        System.out.println("init SClass");
    }
}

public class SubClass extends SClass{
    static{
        System.out.println("SubClass init");
    }

    static int a;

    public SubClass(){
        System.out.println("init SubClass");
    }
}

public class NotInitialization{
    public static void main(String[] args){
        System.out.println(SubClass.value);
    }
}/* Output:
        SSClass
        SClass init!
        123
 *///:~
```
子类引用父类的静态字段只会出发父类的初始化

在本例中，由于value字段是在类SClass中定义的，因此该类会被初始化；此外，在初始化类SClass时，虚拟机会发现其父类SSClass还未被初始化，因此虚拟机将先初始化父类SSClass，然后初始化子类SClass，而SubClass始终不会被初始化。

　2)、通过数组定义来引用类，不会触发此类的初始化

```java
public class NotInitialization{
    public static void main(String[] args){
        SClass[] sca = new SClass[10];
    }
}
```
　上述案例运行之后并没有任何输出

虚拟机没有初始化数组内的类,而是初始化了一个元素类型为SClass的一维数组,它是由虚拟机自动生成的，直接继承于Object的子类，创建动作由字节码指令newarray触发。

3)、常量在编译阶段会存入调用类的常量池中，本质上并没有直接引用到定义常量的类，因此不会触发定义常量的类的初始化
```java
public class ConstClass{

    static{
        System.out.println("ConstClass init!");
    }

    public static  final String CONSTANT = "hello world";
}

public class NotInitialization{
    public static void main(String[] args){
        System.out.println(ConstClass.CONSTANT);
    }
}/* Output:
        hello world
 *///:~

```
　　上述代码运行之后，只输出 “hello world”，这是因为虽然在Java源码中引用了ConstClass类中的常量CONSTANT，但是编译阶段将此常量的值“hello world”存储到了NotInitialization常量池中，对常量ConstClass.CONSTANT的引用实际都被转化为NotInitialization类对自身常量池的引用了。也就是说，实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口，这两个类在编译为Class文件之后就不存在关系了

## 类加载的过程(每一步做了啥)

1、加载（Loading）

　　在加载阶段（可以参考java.lang.ClassLoader的loadClass()方法），虚拟机需要完成以下三件事情：

　　(1). 由加载器通过一个类的全限定名来获取定义此类的二进制字节流（并没有指明要从一个Class文件中获取，可以从其他渠道，譬如：网络、动态生成、数据库等）；

　　(2). 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构；

　　(3). 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象，作为方法区这个类的各种数据的访问入口；

　　加载阶段和连接阶段（Linking）的部分内容（如一部分字节码文件格式验证动作）是交叉进行的，加载阶段尚未完成，连接阶段可能已经开始，但这些夹在加载阶段之中进行的动作，仍然属于连接阶段的内容，这两个阶段的开始时间仍然保持着固定的先后顺序。

2、验证（Verification）

　　验证是连接阶段的第一步，这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。
文件格式验证：验证字节流是否符合Class文件格式的规范(例如，是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型)

元数据验证：对字节码描述的信息进行语义分析，以保证其描述的信息符合Java语言规范的要求(例如：这个类是否有父类，除了java.lang.Object之外)；

字节码验证：通过数据流和控制流分析，确定程序语义是合法的、符合逻辑的;

符号引用验证：确保解析动作能正确执行。

　　验证阶段是非常重要的，但不是必须的，它对程序运行期没有影响。如果所引用的类经过反复验证，那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施，以缩短虚拟机类加载的时间

3、准备(Preparation)
为static变量在方法区分配内存,并设置类变量初始值(一般为0)

这时候内存分配的变量只有类变量,没有实例变量,实例变量会在对象实例化的时候跟随对象实例化一起分配进堆内存.

其次，这里所说的初始值“通常情况”下是数据类型的零值，假设一个类变量的定义为：
```java
 public static int value = 123;
```
 那么，变量value在准备阶段过后的值为0而不是123。因为这时候尚未开始执行任何java方法，而把value赋值为123的putstatic指令是程序被编译后，存放于类构造器方法`<clinit>()`之中，所以把value赋值为123的动作将在初始化阶段才会执行。

 至于“特殊情况”是指：当类字段的字段属性是ConstantValue时，会在准备阶段初始化为指定的值，所以标注为final之后，value的值在准备阶段初始化为123而非0。

 ```java
 　public static final int value = 123;
 ```

4、 解析(Resolution)

　　解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

5、初始化(Initialization)

　　类初始化阶段是类加载过程的最后一步。在前面的类加载过程中，除了在加载阶段用户应用程序可以通过自定义类加载器参与之外，其余动作完全由虚拟机主导和控制。到了初始化阶段，才真正开始执行类中定义的java程序代码(字节码)。

在准备阶段，变量已经赋过一次系统要求的初始值(零值)；而在初始化阶段，则根据程序猿通过程序制定的主观计划去初始化类变量和其他资源，或者更直接地说：初始化阶段是执行类构造器`<clinit>()`方法的过程。`<clinit>()`方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的，编译器收集的顺序是由语句在源文件中出现的顺序所决定的，静态语句块只能访问到定义在静态语句块之前的变量，定义在它之后的变量，在前面的静态语句块可以赋值，但是不能访问。如下：

```java
public class Test{
    static{
        i=0;
        System.out.println(i);//Error：Cannot reference a field before it is defined（非法向前应用）
    }
    static int i=1;
}

```
修改为下面就可以正常运行了：
```java
public class Test{
    static{
        i=0;
        //System.out.println(i);
    }

    static int i=1;

    public static void main(String args[]){
        System.out.println(i);
    }
}/* Output:
        1
 *///:~
```

 类构造器`<clinit>()`与实例构造器`<init>()`不同，它不需要程序员进行显式调用

 虚拟机会保证在子类类构造器`<clinit>()`执行之前，父类的类构造`<clinit>()`执行完毕。由于父类的构造器`<clinit>()`先执行，也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。

 特别地，类构造器`<clinit>()`对于类或者接口来说并不是必需的，如果一个类中没有静态语句块，也没有对类变量的赋值操作，那么编译器可以不为这个类生产类构造器`<clinit>()`。

　虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步，如果多个线程同时去初始化一个类，那么只会有一个线程去执行这个类的类构造器<clinit>()，其他线程都需要阻塞等待，直到活动线程执行<clinit>()方法完毕。

当执行的线程初始化完成后，其他阻塞线程不会再执行初始化，因为**在同一个类加载器下，一个类型只会被初始化一次**'
```java
public class DealLoopTest {
    static{
        System.out.println("DealLoopTest...");
    }
    static class DeadLoopClass {
        static {
            if (true) {
                System.out.println(Thread.currentThread()
                        + "init DeadLoopClass");
                while (true) {      // 模拟耗时很长的操作
                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = new Runnable() {   // 匿名内部类
            public void run() {
                System.out.println(Thread.currentThread() + " start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over");
            }
        };

        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}/* Output:
        DealLoopTest...
        Thread[Thread-1,5,main] start
        Thread[Thread-0,5,main] start
        Thread[Thread-1,5,main]init DeadLoopClass
 *///:~
```
 如上述代码所示，在初始化DeadLoopClass类时，线程Thread-1得到执行并在执行这个类的类构造器`<clinit>()` 时，由于该方法包含一个死循环，因此久久不能退出。

**创建一个对象常常需要经历如下几个过程：父类的类构造器`<clinit>() `-> 子类的类构造器`<clinit>() `-> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。**

举例：
```java
public class StaticTest {
    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {   //静态代码块
        System.out.println("1");
    }

    {       // 实例代码块
        System.out.println("2");
    }

    StaticTest() {    // 实例构造器
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {   // 静态方法
        System.out.println("4");
    }

    int a = 110;    // 实例变量
    static int b = 112;     // 静态变量
    //static final int b=112
    //那么，在准备阶段b的值就是112，而不再是0了。
}/* Output:
        2
        3
        a=110,b=0
        1
        4
 *///:~
```
解释：

一般思路是：
准备阶段
先常量直接赋值（准备阶段转移到常量类），再 静态变量，静态代码（赋默认值0）

初始化阶段
父类的类构造器`<clinit>()` -> 子类的类构造器`<clinit>() `-> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。

`<clinit>()`方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的，编译器收集的顺序是由语句在源文件中出现的顺序所决定的，静态语句块只能访问到定义在静态语句块之前的变量，定义在它之后的变量，在前面的静态语句块可以赋值，但是不能访问。

此题需要注意：
在初始化阶段，当JVM对类StaticTest进行初始化时，首先会执行下面的语句：
```java
static StaticTest st = new StaticTest();
```
也就是实例化StaticTest对象，但这个时候类都没有初始化完毕啊，能直接进行实例化吗？事实上，这涉及到一个根本问题就是：实例初始化不一定要在类初始化结束之后才开始初始化。 下面我们结合类的加载过程说明这个问题。

此时，就碰到了笔者上面的疑惑，即“在类都没有初始化完毕之前，能直接进行实例化相应的对象吗？”。事实上，从Java角度看，我们知道一个类初始化的基本常识，那就是：在同一个类加载器下，一个类型只会被初始化一次。所以，一旦开始初始化一个类型，无论是否完成，后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此，在实例化上述程序中的st变量时，实际上是把实例初始化嵌入到了静态初始化流程中，并且在上面的程序中，嵌入到了静态初始化的起始位置。这就导致了实例初始化完全发生在静态初始化之前，当然，这也是导致a为110b为0的原因。

因此，上述程序的StaticTest类构造器`<clinit>()`的实现等价于：
```java
public class StaticTest {
    <clinit>(){
        a = 110;    // 实例变量
        System.out.println("2");        // 实例代码块
        System.out.println("3");     // 实例构造器中代码的执行
        System.out.println("a=" + a + ",b=" + b);  // 实例构造器中代码的执行
        类变量st被初始化
        System.out.println("1");        //静态代码块
        类变量b被初始化为112
    }
}
```
下面，我们对上述程序稍作改动，如下所示：

```java
public class StaticTest {
    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest() {
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {
        System.out.println("4");
    }

    int a = 110;
    static int b = 112;
    static StaticTest st1 = new StaticTest();
}
```
在程序最后的一行，增加代码行：static StaticTest st1 = new StaticTest();

```
输出变成了
2
3
a=110,b=0
1
2
3
a=110,b=112
4
```

关于方法的重写：
```java

class Foo {
    int i = 1;

    Foo() {
        System.out.println(i);
        int x = getValue();
        System.out.println(x);
    }

    {
        i = 2;
    }

    protected int getValue() {
        return i;
    }
}

//子类
class Bar extends Foo {
    int j = 1;

    Bar() {
        j = 2;
    }

    {
        j = 3;
    }

    @Override
    protected int getValue() {
        return j;
    }
}

public class ConstructorExample {
    public static void main(String... args) {
        Bar bar = new Bar();
        System.out.println(bar.getValue());
    }
}
输出结果是

2
0
2
这样程序就好看多了，我们一眼就可以观察出程序的输出结果。在通过使用Bar类的构造方法new一个Bar类的实例时，首先会调用Foo类构造函数，因此(1)处输出是2，这从Foo类构造函数的等价变换中可以直接看出。

(2)处输出是0，为什么呢？因为在执行Foo的构造函数的过程中，由于Bar重载了Foo中的getValue方法，所以根据Java的多态特性可以知道，其调用的getValue方法是被Bar重载的那个getValue方法。但由于这时Bar的构造函数还没有被执行，因此此时j的值还是默认值0，因此(2)处输出是0。

最后，在执行(3)处的代码时，由于bar对象已经创建完成，所以此时再访问j的值时，就得到了其初始化后的值2，这一点可以从Bar类构造函数的等价变换中直接看出。
```

## 加载后 类的实例化与初始化
一个Java对象的创建过程往往包括 类初始化 和 类实例化 两个阶段。

实例化对象之前需要先加载类并通过类构造器进行初始化

### 创建对象的5种方式,两种不需要构造器
1). 使用new关键字创建对象

这是我们最常见的也是最简单的创建对象的方式，通过这种方式我们可以调用任意的构造函数（无参的和有参的）去创建对象。比如：

使用new关键字来调用一个类的构造函数显式地创建对象，这种方式在Java规范中被称为 : 由执行类实例创建表达式而引起的对象创建。

```java
　Student student = new Student();
```
2). 使用Class类的newInstance方法(反射机制)

其实newinstance调用的是无参构造器

```java
　　Student student2 = (Student)Class.forName("Student类全限定名").newInstance();
或者：
　　Student stu = Student.class.newInstance();
```

3). 使用Constructor类的newInstance方法(反射机制)

　java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象，该方法和Class类中的newInstance方法很像，但是相比之下，Constructor类的newInstance方法更加强大些，我们可以通过这个newInstance方法调用有参数的和私有的构造函数

```java
public class Student {

    private int id;

    public Student(Integer id) {
        this.id = id;
    }

    public static void main(String[] args) throws Exception {

        Constructor<Student> constructor = Student.class
                .getConstructor(Integer.class);
        Student stu3 = constructor.newInstance(123);
    }
}
```

使用newInstance方法的这两种方式创建对象使用的就是Java的反射机制，事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法。
4). 使用Clone方法创建对象

　无论何时我们调用一个对象的clone方法，JVM都会帮我们创建一个新的、一样的对象，特别需要说明的是，用clone方法创建对象的过程中并不会调用任何构造函数。

简单而言，要想使用clone方法，我们就必须先实现Cloneable接口并实现其定义的clone方法，这也是原型模式的应用。比如：
```java
public class Student implements Cloneable{

    private int id;

    public Student(Integer id) {
        this.id = id;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }

    public static void main(String[] args) throws Exception {

        Constructor<Student> constructor = Student.class
                .getConstructor(Integer.class);
        Student stu3 = constructor.newInstance(123);
        Student stu4 = (Student) stu3.clone();
    }
}
```

5). 使用(反)序列化机制创建对象

VM并不会调用任何构造函数。为了反序列化一个对象，我们需要让我们的类实现Serializable接口，比如：
```java
public class Student implements Cloneable, Serializable {

    private int id;

    public Student(Integer id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Student [id=" + id + "]";
    }

    public static void main(String[] args) throws Exception {

        Constructor<Student> constructor = Student.class
                .getConstructor(Integer.class);
        Student stu3 = constructor.newInstance(123);

        // 写对象
        ObjectOutputStream output = new ObjectOutputStream(
                new FileOutputStream("student.bin"));
        output.writeObject(stu3);
        output.close();

        // 读对象
        ObjectInputStream input = new ObjectInputStream(new FileInputStream(
                "student.bin"));
        Student stu5 = (Student) input.readObject();
        System.out.println(stu5);
    }
}
```

综合上述

```java
public class Student implements Cloneable, Serializable {

    private int id;

    public Student() {

    }

    public Student(Integer id) {
        this.id = id;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }

    @Override
    public String toString() {
        return "Student [id=" + id + "]";
    }

    public static void main(String[] args) throws Exception {

        System.out.println("使用new关键字创建对象：");
        Student stu1 = new Student(123);
        System.out.println(stu1);
        System.out.println("\n---------------------------\n");

        System.out.println("使用Class类的newInstance方法创建对象：");
        Student stu2 = Student.class.newInstance();    //对应类必须具有无参构造方法，且只有这一种创建方式
        System.out.println(stu2);
        System.out.println("\n---------------------------\n");

        System.out.println("使用Constructor类的newInstance方法创建对象：");
        Constructor<Student> constructor = Student.class
                .getConstructor(Integer.class);   // 调用有参构造方法
        Student stu3 = constructor.newInstance(123);
        System.out.println(stu3);
        System.out.println("\n---------------------------\n");

        System.out.println("使用Clone方法创建对象：");
        Student stu4 = (Student) stu3.clone();
        System.out.println(stu4);
        System.out.println("\n---------------------------\n");

        System.out.println("使用(反)序列化机制创建对象：");
        // 写对象
        ObjectOutputStream output = new ObjectOutputStream(
                new FileOutputStream("student.bin"));
        output.writeObject(stu4);
        output.close();

        // 读取对象
        ObjectInputStream input = new ObjectInputStream(new FileInputStream(
                "student.bin"));
        Student stu5 = (Student) input.readObject();
        System.out.println(stu5);

    }
}/* Output:
        使用new关键字创建对象：
        Student [id=123]

        ---------------------------

        使用Class类的newInstance方法创建对象：
        Student [id=0]

        ---------------------------

        使用Constructor类的newInstance方法创建对象：
        Student [id=123]

        ---------------------------

        使用Clone方法创建对象：
        Student [id=123]

        ---------------------------

        使用(反)序列化机制创建对象：
        Student [id=123]
*///:~
```
　从Java虚拟机层面看，除了使用new关键字创建对象的方式外，其他方式全部都是通过转变为invokevirtual指令直接创建对象的。

### 创建对象的过程
分配内存
对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时，这些实例变量也会被赋予默认值(零值)。

初始化
在内存分配完成之后，Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中，主要涉及三种执行对象初始化的结构，分别是实例变量初始化、实例代码块初始化 以及 构造函数初始化。

1、实例变量初始化与实例代码块初始化

我们在定义（声明）实例变量的同时，还可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。如果我们以这两种方式为实例变量进行初始化，那么它们将**在构造函数执行之前**完成这些初始化操作。

实际上，如果我们对实例变量直接赋值或者使用实例代码块赋值，那么编译器会将其中的代码放到类的构造函数中去，**并且这些代码会被放在对超类构造函数的调用语句之后(还记得吗？Java要求构造函数的第一条语句必须是超类构造函数的调用语句)，构造函数本身的代码之前**。

```java
public class InstanceVariableInitializer {

    private int i = 1;
    private int j = i + 1;

    public InstanceVariableInitializer(int var){
        System.out.println(i);
        System.out.println(j);
        this.i = var;
        System.out.println(i);
        System.out.println(j);
    }

    {               // 实例代码块
        j += 3;

    }

    public static void main(String[] args) {
        new InstanceVariableInitializer(8);
    }
}/* Output:
            1
            5
            8
            5
 *///:~
```

 　上面的例子正好印证了上面的结论。特别需要注意的是，Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的，并且**不允许顺序靠前的实例代码块初始化在其后面定义的实例变量**，比如：

```java
public class InstanceInitializer {
    {
        j = i;
    }

    private int i = 1;
    private int j;
}

public class InstanceInitializer {
    private int j = i;
    private int i = 1;
}
```
上面的这些代码都是无法通过编译的，编译器会抱怨说我们使用了一个未经定义的变量。之所以要这么做是为了保证一个变量在被使用之前已经被正确地初始化。

但是我们仍然有办法绕过这种检查，比如：

```java
public class InstanceInitializer {
    private int j = getI();
    private int i = 1;

    public InstanceInitializer() {
        i = 2;
    }

    private int getI() {
        return i;
    }

    public static void main(String[] args) {
        InstanceInitializer ii = new InstanceInitializer();
        System.out.println(ii.j);
    }
}
```
如果我们执行上面这段代码，那么会发现打印的结果是0。因此我们可以确信，变量j被赋予了i的默认值0，这一动作发生在实例变量i初始化之前和构造函数调用之前。

2、构造函数初始化

实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前

众所周知，每一个Java中的对象都至少会有一个构造函数，如果我们没有显式定义构造函数，那么它将会有一个默认无参的构造函数。在编译生成的字节码中，这些**构造函数**会被命名成`<init>()`方法，参数列表与Java语言书写的构造函数的参数列表相同。

 而**类构造器**的名字是`<clinit>()`;

Java要求在实例化类之前，必须先实例化其超类，以保证所创建实例的完整性。事实上，这一点是在构造函数中保证的：Java强制要求Object对象(Object是Java的顶层对象，没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数，如果我们既没有调用其他的构造函数，也没有显式调用超类的构造函数，那么编译器会为我们自动生成一个对超类构造函数的调用
```java
public class ConstructorExample {

}
```
对于上面代码中定义的类，我们观察编译之后的字节码，我们会发现编译器为我们生成一个构造函数，如下，

```java
aload_0
invokespecial   #8; //Method java/lang/Object."<init>":()V
return
```
上面代码的第二行就是调用Object类的默认构造函数的指令。也就是说，如果我们显式调用超类的构造函数，那么该调用必须放在构造函数所有代码的最前面，也就是必须是构造函数的第一条指令。正因为如此，Java才可以使得一个对象在初始化之前其所有的超类都被初始化完成，并保证创建一个完整的对象出来。

如果我们在一个构造函数中调用另外一个构造函数，如下所示，

```java
public class ConstructorExample {
    private int i;

    ConstructorExample() {
        this(1);
        ....
    }

    ConstructorExample(int i) {
        ....
        this.i = i;
        ....
    }
}
```
对于这种情况，Java只允许在ConstructorExample(int i)内调用超类的构造函数，也就是说，下面两种情形的代码编译是无法通过的：

```java
public class ConstructorExample {
    private int i;

    ConstructorExample() {
        super();
        this(1);  // Error:Constructor call must be the first statement in a constructor
        ....
    }

    ConstructorExample(int i) {
        ....
        this.i = i;
        ....
    }
}

public class ConstructorExample {
    private int i;

    ConstructorExample() {
        this(1);
        super();  //Error: Constructor call must be the first statement in a constructor
        ....
    }

    ConstructorExample(int i) {
        this.i = i;
    }
}

```

Java通过对构造函数作出这种限制以便保证一个类的实例能够在被使用之前正确地初始化。

### 实例化小结: 实例化就是对象实例变量,代码,构造器的初始化,字节码中是init函数(要与静态变量,代码的初始化区分开,字节码中是clinit函数)

实例化一个类的对象的过程是一个典型的递归过程

在准备实例化一个类的对象前，首先准备实例化该类的父类，如果该类的父类还有父类，那么准备实例化该类的父类的父类，依次递归直到递归到Object类。此时，首先实例化Object类，再依次对以下各类进行实例化，直到完成对目标类的实例化。具体而言，在实例化每个类时，都遵循如下顺序：先依次执行实例变量初始化和实例代码块初始化，再执行构造函数初始化。也就是说，编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去，并且这些代码会被放在对超类构造函数的调用语句之后，构造函数本身的代码之前。

### this逃逸

this逃逸就是说**在构造函数返回之前其他线程就持有该对象的引用，调用尚未构造完全的对象的方法可能引发错误。**

## 区分 准备阶段,初始化阶段,实例化阶段

在类加载过程中，准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值（零值）的阶段，

而初始化阶段是真正开始执行类中定义的java程序代码(字节码)并按程序猿的意图去初始化类变量的过程。更直接地说，初始化阶段就是执行类构造器`<clinit>()`方法的过程。`<clinit>()`方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{}中的语句合并产生的，其中编译器收集的顺序是由语句在源文件中出现的顺序所决定。

初始化阶段完成后,当创建一个对象的时候(new,class.instance,反射的constructor.newinstance,clone,反序列化)会触发对象的实例化`<init>()`

类构造器`<clinit>()`与实例构造器`<init>()`不同，它不需要程序员进行显式调用，虚拟机会保证在子类类构造器`<clinit>`()执行之前，父类的类构造`<clinit>()`执行完毕。由于父类的构造器`<clinit>()`先执行，也就意味着父类中定义的静态代码块/静态变量的初始化要优先于子类的静态代码块/静态变量的初始化执行。

特别地，**类构造器`<clinit>()`对于类或者接口来说并不是必需的**，如果一个类中没有静态代码块，也没有对类变量的赋值操作，那么编译器可以不为这个类生产类构造器`<clinit>()`。此外，在同一个类加载器下，一个类只会被初始化一次，但是一个类可以任意地实例化对象。也就是说，**在一个类的生命周期中，类构造器`<clinit>()`最多会被虚拟机调用一次，而实例构造器`<init>()`则会被虚拟机调用多次，只要程序员还在创建对象。**

### 常见考题

 一个实例变量在对象初始化的过程中会被赋值几次？

　　我们知道，JVM在为一个对象分配完内存之后，会给每一个实例变量赋予默认值，这个时候实例变量被第一次赋值，这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作，那么这个时候，这个实例变量就被第二次赋值了。如果我们在实例代码块中，又对变量x做了初始化操作，那么这个时候，这个实例变量就被第三次赋值了。如果我们在构造函数中，也对变量x做了初始化操作，那么这个时候，变量x就被第四次赋值。也就是说，在Java的对象初始化过程中，一个实例变量最多可以被初始化4次。

　2、类的初始化过程与类的实例化过程的异同？

　　函数  clinit init
    一次   多次
    静态   实例
    非必须  必须

　　3、假如一个类还未加载到内存中，那么在创建一个该类的实例时，具体过程是怎样的？

　　我们知道，要想创建一个类的实例，必须先将该类加载到内存并进行初始化，也就是说，类初始化操作是在类实例化操作之前进行的，**但并不意味着**：**只有类初始化操作结束后才能进行类实例化操作。**

上文提到过的经典案例
```java

public class StaticTest {
    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {   //静态代码块
        System.out.println("1");
    }

    {       // 实例代码块
        System.out.println("2");
    }

    StaticTest() {    // 实例构造器
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {   // 静态方法
        System.out.println("4");
    }

    int a = 110;    // 实例变量
    static int b = 112;     // 静态变量
}/* Output:
        2
        3
        a=110,b=0
        1
        4
 *///:~
```
 参考博文: https://blog.csdn.net/justloveyou_/article/details/72466416

## 类加载器

[https://blog.csdn.net/justloveyou_/article/details/72217806](https://blog.csdn.net/justloveyou_/article/details/72217806)

启动（Bootstrap）类加载器：启动类加载器是用本地代码实现的类加载器，它负责将JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。由于启动类加载器涉及到虚拟机本地实现细节，开发者无法直接获取到启动类加载器的引用。具体可由启动类加载器加载到的路径可通过System.getProperty(“sun.boot.class.path”)查看。

扩展（Extension）类加载器：扩展类加载器是由Sun的ExtClassLoader（sun.misc.Launcher$ExtClassLoader）实现的，它负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器，具体可由扩展类加载器加载到的路径可通过System.getProperty("java.ext.dirs")查看。

系统（System）类加载器：系统类加载器是由 Sun 的 AppClassLoader（sun.misc.Launcher$AppClassLoader）实现的，它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录，即当前类所在路径及其引用的第三方类库的路径，如第四节中的问题6所述)下的类库加载到内存中。开发者可以直接使用系统类加载器，具体可由系统类加载器加载到的路径可通过System.getProperty("java.class.path")查看。

### 双亲委派机制
JVM在加载类时默认采用的是双亲委派机制。通俗的讲，就是某个特定的类加载器在接到加载类的请求时，首先将加载任务委托给父类加载器，依次递归 (本质上就是loadClass函数的递归调用)，因此所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求，就成功返回；只有当父类加载器无法完成此加载请求时，子加载器才会尝试自己去加载。

上面两张图分别是扩展类加载器继承层次图和系统类加载器继承层次图。通过这两张图我们可以看出，扩展类加载器和系统类加载器均是继承自 java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下抽象类 java.lang.ClassLoader 中几个最重要的方法：

面试题:findclass loadclass defineclass的区别

[https://blog.csdn.net/mollen/article/details/100840940](https://blog.csdn.net/mollen/article/details/100840940)

loadClass()
findLoadedClass(String) 调用这个方法，查看这个Class是否已经别加载

如果没有被加载，继续往下走，查看父类加载器，递归调用loadClass()

如果父类加载器是null，说明是启动类加载器，查找对应的Class

如果都没有找到，就调用findClass(String)

findClass()
根据名称或位置加载.class字节码,然后使用defineClass

definclass()
把字节码转化为Class

```java
//加载指定名称（包括包名）的二进制类型，供用户调用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }

//加载指定名称（包括包名）的二进制类型，同时指定是否解析（但是这里的resolve参数不一定真正能达到解析的效果），供继承用
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }

//findClass方法一般被loadClass方法调用去加载指定名称类，供继承用
protected Class<?> findClass(String name) throws ClassNotFoundException { … }

//定义类型，一般在findClass方法中读取到对应字节码后调用，final的，不能被继承
//这也从侧面说明：JVM已经实现了对应的具体功能，解析对应的字节码，产生对应的内部数据结构放置到方法区，所以无需覆写，直接调用就可以了）
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }

```

标准扩展类加载器和系统类加载器的代码以及其公共父类（java.net.URLClassLoader和java.security.SecureClassLoader）的代码可以看出，都没有覆写java.lang.ClassLoader中默认的加载委派规则 — loadClass（…）方法。既然这样，我们就可以从java.lang.ClassLoader中的loadClass（String name）方法的代码中分析出虚拟机默认采用的双亲委派机制到底是什么模样：

```java
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {

    // 首先判断该类型是否已经被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        //如果没有被加载，就委托给父类加载或者委派给启动类加载器加载
        try {
            if (parent != null) {
                //如果存在父类加载器，就委派给父类加载器加载
                c = parent.loadClass(name, false);
            } else {    // 递归终止条件
                // 由于启动类加载器无法被Java程序直接引用，因此默认用 null 替代
                // parent == null就意味着由启动类加载器尝试加载该类，
                // 即通过调用 native方法 findBootstrapClass0(String name)加载
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父类加载器不能完成加载请求时，再调用自身的findClass方法进行类加载，若加载成功，findClass方法返回的是defineClass方法的返回值
            // 注意，若自身也加载不了，会产生ClassNotFoundException异常并向上抛出
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

```

### 运行时动态扩展
Java的连接模型允许用户运行时扩展引用程序，既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口，又允许用户自行定义类装载器，在运行时动态扩展用户的程序。通过用户自定义的类装载器，你的程序可以加载在编译时并不知道或者尚未存在的类或者接口，并动态连接它们并进行有选择的解析。运行时动态扩展java应用程序有如下两个途径：

#### 反射 (调用java.lang.Class.forName(…)加载类)

**Class.forName(String name)默认会使用调用类的类加载器来进行类加载。**

```java
//java.lang.Class.java
publicstatic Class<?> forName(String className) throws ClassNotFoundException {
    return forName0(className, true, ClassLoader.getCallerClassLoader());
}

//java.lang.ClassLoader.java
// Returns the invoker's class loader, or null if none.
static ClassLoader getCallerClassLoader() {
    // 获取调用类（caller）的类型
    Class caller = Reflection.getCallerClass(3);
    // This can be null if the VM is requesting it
    if (caller == null) {
        return null;
    }
    // 调用java.lang.Class中本地方法获取加载该调用类（caller）的ClassLoader
    return caller.getClassLoader0();
}

//java.lang.Class.java
//虚拟机本地实现，获取当前类的类加载器，前面介绍的Class的getClassLoader()也使用此方法
native ClassLoader getClassLoader0();

```

**这里需要说明的是多参数版本的forName(…)方法：**

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException

这里的initialize参数是很重要的，它表示在加载同时是否完成初始化的工作（说明：单参数版本的forName方法默认是完成初始化的）。有些场景下需要将initialize设置为true来强制加载同时完成初始化，例如典型的就是加载数据库驱动问题。因为JDBC驱动程序只有被注册后才能被应用程序使用，这就要求驱动程序类必须被初始化，而不单单被加载。

#### 自定义类加载器
用户自定义类加载器

　　通过前面的分析，我们可以看出，除了和本地实现密切相关的启动类加载器之外，包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待，唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍，这里就简要叙述一下一般用户自定义类加载器的工作流程（可以结合后面问题解答一起看）：

1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了，如果已经装载，直接返回；否则转入步骤2；

2、委派类加载请求给父类加载器（更准确的说应该是双亲类加载器，真实虚拟机中各种类加载器最终会呈现树状结构），如果父类加载器能够完成，则返回父类加载器加载的Class实例；否则转入步骤3；

3、调用本类加载器的findClass（…）方法，试图获取对应的字节码。如果获取的到，则调用defineClass（…）导入类型到方法区；如果获取不到对应的字节码或者其他原因失败， 向上抛异常给loadClass（…）， loadClass（…）转而调用findClass（…）方法处理异常，直至完成递归调用。

必须指出的是，这里所说的自定义类加载器是指JDK1.2以后版本的写法，即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下。整个加载类的过程如下图：

### 类加载器常见问题

#### 由不同的类加载器加载的指定类还是相同的类型吗？

在Java中，一个类用其完全匹配类名(fully qualified class name)作为标识，这里指的完全匹配类名包括包名和类名。但在JVM中，一个类用其全名 和 一个ClassLoader的实例作为唯一标识，不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型（注意不要将自定义类型的字节码放置到系统路径或者扩展路径中，否则会被系统类加载器或扩展类加载器抢先加载），然后用获取到的两个Class实例进行java.lang.Object.equals（…）判断，将会得到不相等的结果，如下所示：
```java
public class TestBean {

	public static void main(String[] args) throws Exception {
	    // 一个简单的类加载器，逆向双亲委派机制
	    // 可以加载与自己在同一路径下的Class文件
		ClassLoader myClassLoader = new ClassLoader() {
			@Override
			public Class<?> loadClass(String name)
					throws ClassNotFoundException {
				try {
					String filename = name.substring(name.lastIndexOf(".") + 1)
							+ ".class";
					InputStream is = getClass().getResourceAsStream(filename);
					if (is == null) {
						return super.loadClass(name);   // 递归调用父类加载器
					}
					byte[] b = new byte[is.available()];
					is.read(b);
					return defineClass(name, b, 0, b.length);
				} catch (Exception e) {
					throw new ClassNotFoundException(name);
				}
			}
		};

		Object obj = myClassLoader.loadClass("classloader.test.bean.TestBean")
				.newInstance();
		System.out.println(obj.getClass());
		System.out.println(obj instanceof classloader.test.bean.TestBean);
	}
}/* Output:
        class classloader.test.bean.TestBean
        false
 *///:~
```

我们发现，obj 确实是类classloader.test.bean.TestBean实例化出来的对象，但当这个对象与类classloader.test.bean.TestBean做所属类型检查时却返回了false。这是因为虚拟机中存在了两个TestBean类，一个是由系统类加载器加载的，另一个则是由我们自定义的类加载器加载的，虽然它们来自同一个Class文件，但依然是两个独立的类，因此做所属类型检查时返回false。

#### 在编写自定义类加载器时，如果没有设定父加载器，那么父加载器是谁？

自定义类加载器没有指定父类加载器的情况下，默认的父类加载器即为系统类加载器。同时，我们可以得出如下结论：即使用户自定义类加载器不指定父类加载器，那么，同样可以加载如下三个地方的类：

<Java_Runtime_Home>/lib下的类；
<Java_Runtime_Home>/lib/ext下或者由系统变量java.ext.dir指定位置中的类；
当前工程类路径下或者由系统变量java.class.path指定位置中的类。

#### 在编写自定义类加载器时，如果将父类加载器强制设置为null，那么会有什么影响？如果自定义的类加载器不能加载指定类，就肯定会加载失败吗？
因为启动类加载器的代表就是null;

JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null，那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器（这个问题前面已经分析过了）。同时，我们可以得出如下结论：即使用户自定义类加载器不指定父类加载器，那么，同样可以加载到<JAVA_HOME>/lib下的类，但此时就不能够加载<JAVA_HOME>/lib/ext目录下的类了。

#### 如何在运行时判断系统类加载器能加载哪些路径下的类？

一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器（系统类加载器和扩展类加载器本身都派生自URLClassLoader），调用URLClassLoader中的getURLs()方法可以获取到。二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 ：System.getProperty(“java.class.path”)。如下所示，

```java
public class Test {
	public static void main(String[] args) {
		System.out.println("Rico");
		Gson gson = new Gson();
		System.out.println(gson.getClass().getClassLoader());
		System.out.println(System.getProperty("java.class.path"));
	}
}/* Output:
        Rico
		sun.misc.Launcher$AppClassLoader@6c68bcef
		I:\AlgorithmPractice\TestClassLoader\bin;I:\Java\jars\Gson\gson-2.3.1.jar
 *///:~

```
如上述程序所示，Test类和Gson类由系统类加载器加载，并且其加载路径就是用户类路径，包括当前类路径和引用的第三方类库的路径。

### 再理解双亲委派
在前面介绍类加载器的代理委派模型的时候，提到过类加载器会首先代理给其它类加载器来尝试加载某个类，这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器，有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的；而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器（defining loader），后者称为初始加载器（initiating loader）。在Java虚拟机判断两个类是否相同的时候，使用的是类的定义加载器。也就是说，哪个类加载器启动类的加载过程并不重要，重要的是最终定义这个类的加载器。两种类加载器的关联之处在于：一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner，则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

类加载器在成功加载某个类之后，会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候，类加载器会直接使用缓存的类的实例，而不会尝试再次加载。也就是说，对于一个类加载器实例来说，相同全名的类只加载一次，即 loadClass方法不会被重复调用。

在绝大多数情况下，系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下，您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码，为了保证安全性，这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码，接着进行解密和验证，最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

[文件系统类加载器,网络系统类加载器见博客最底部](https://blog.csdn.net/justloveyou_/article/details/72217806)

 # 强引用,软引用,弱引用,虚引用

 ## 强引用
当内存空间不足时，Java虚拟机宁愿抛出OutOfMemoryError错误，使程序异常终止，也不会靠随意回收具有强引用的对象来解决内存不足的问题。

如果强引用对象不使用时，需要弱化从而使GC能够回收，如下：
```java
    strongReference = null;
```
显式地设置**strongReference对象为null**，或**让其超出对象的生命周期范围**，则gc认为该对象不存在引用，这时就可以回收这个对象。具体什么时候收集这要取决于GC算法。

所谓超出对象生命周期范围:
在test方法的内部有一个强引用，这个引用保存在Java栈中，而真正的引用内容(Object)保存在Java堆中。 当这个方法运行完成后，就会退出方法栈，则引用对象的引用数为0，这个对象会被回收。

```java
  public void test() {
        Object strongReference = new Object();
        // 省略其他操作
    }
```

但是如果这个strongReference是全局变量时，就需要在不用这个对象时赋值为null，因为强引用不会被垃圾回收。

### 数组的取消强引用

在ArrayList类中定义了一个elementData数组，在调用clear方法清空数组时，每个数组元素被赋值为null。
不同于elementData=null，强引用仍然存在，避免在后续调用add()等方法添加元素时进行内存的重新分配。
使用如clear()方法内存数组中存放的引用类型进行内存释放特别适用，这样就可以及时释放内存。

 ## 软引用
应用场景: 软引用可用来实现内存敏感的高速缓存。

被软引用关联的对象只有在内存不够的情况下才会被回收。

使用 SoftReference 类来创建软引用。

```java
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联
```

应用场景：

浏览器的后退按钮。按后退时，这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢？这就要看具体的实现策略了。

```java
    // 获取浏览器对象进行浏览
    Browser browser = new Browser();
    // 从后台程序加载浏览页面
    BrowserPage page = browser.getPage();
    // 将浏览完毕的页面置为软引用
    SoftReference softReference = new SoftReference(page);

    // 回退或者再次浏览此页面时
    if(softReference.get() != null) {
        // 内存充足，还没有被回收器回收，直接获取缓存
        page = softReference.get();
    } else {
        // 内存不足，软引用的对象已经回收
        page = browser.getPage();
        // 重新构建软引用
        softReference = new SoftReference(page);
    }

```

 ## 弱引用

 被弱引用关联的对象一定会被回收，也就是说它只能存活到下一次垃圾回收发生之前。由于垃圾回收器是一个优先级很低的线程，因此不一定会很快发现那些只具有弱引用的对象。

使用 WeakReference 类来创建弱引用。

```java
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
```

下面的代码会让一个弱引用再次变为一个强引用：
通过用一个新的引用来指向 弱引用返回的对象
```java
    String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    // 弱引用转强引用
    String strongReference = weakReference.get();
```

 ## 虚引用

又称为幽灵引用或者幻影引用，一个对象是否有虚引用的存在，不会对其生存时间造成影响，也无法通过虚引用得到一个对象。

为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

使用 PhantomReference 来创建虚引用。

```java
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
```

 ## 引用队列

 当gc（垃圾回收线程）准备回收一个对象时，如果发现它只有软引用(或弱引用，或虚引用)指向它，就会在回收该对象之前，把这个软引用（或弱引用，或虚引用）加入到与之关联的引用队列（ReferenceQueue）中。

当软引用（或弱引用，或虚引用）对象所指向的对象被回收了，那么这个引用对象本身就没有价值了，如果程序中存在大量的这类对象（注意，我们创建的软引用、弱引用、虚引用对象本身是个强引用，不会自动被gc回收），就会浪费内存。因此我们这就可以手动回收位于引用队列中的引用对象本身。

 # java逃逸分析

## JIT:Just-In-Time Compiler，即时编译器
java编译体系中,java源代码转换成可执行的机器代码文件需要经过两步:
第一步:
源代码转换成.class文件(字节码)
第二步
.class文件被解释成机器码.经过解释执行，其执行速度必然会比可执行的二进制字节码程序慢很多。这就是传统的JVM的解释器（Interpreter）的功能。为了解决这种效率问题，引入了 JIT（即时编译） 技术。

引入了 JIT 技术后，Java程序还是通过解释器进行解释执行，当JVM发现某个方法或代码块运行特别频繁的时候，就会认为这是“热点代码”（Hot Spot Code)。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码，并进行优化，然后再把翻译后的机器码缓存起来，以备下次使用。

JIT可以进行编译,热点检测和优化,其中优化最重要的一个就是逃逸分析。
## 逃逸分析定义

是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析，Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否进行同步省略,标量替换,栈上引用等优化方法

逃逸分析的基本行为就是分析对象动态作用域：当一个对象在方法中被定义后，它可能被外部方法所引用，例如作为调用参数传递到其他地方中，称为方法逃逸。

```java
public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}
```
第一段代码中的sb就逃逸了，而第二段代码中的sb就没有逃逸。因为第一段把sb传给了外面,导致外面也可以使用.

使用逃逸分析，编译器可以对代码做如下优化：

一、同步省略。如果一个对象被发现只能从一个线程被访问到，那么对于这个对象的操作可以不考虑同步。

二、将堆分配转化为栈分配。如果一个对象在子程序中被分配，要使指向该对象的指针永远不会逃逸，对象可能是栈分配的候选，而不是堆分配。

三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到，那么对象的部分（或全部）可以不存储在内存，而是存储在CPU寄存器中。

在Java代码运行时，通过JVM参数可指定是否开启逃逸分析，

-XX:+DoEscapeAnalysis ： 表示开启逃逸分析

-XX:-DoEscapeAnalysis ： 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析，如需关闭，需要指定-XX:-DoEscapeAnalysis

## 同步省略

在动态编译同步块的时候，JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。

如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问，那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略，也叫锁消除。

```java
public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}
```
代码中对hollis这个对象进行加锁，但是hollis对象的生命周期只在f()方法中，并不会被其他线程所访问到，所以在JIT编译阶段就会被优化掉。优化成：

```java
public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}
```
## 标量替换
标量（Scalar）是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的，那些还可以分解的数据叫做聚合量（Aggregate），Java中的对象就是聚合量，因为他可以分解成其他聚合量和标量。

在JIT阶段，如果经过逃逸分析，发现一个对象不会被外界访问的话，那么经过JIT优化，就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

```java
public static void main(String[] args) {
   alloc();
}

private static void alloc() {
   Point point = new Point（1,2）;
   System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
    private int x;
    private int y;
}
```
以上代码中，point对象并没有逃逸出alloc方法，并且point对象是可以拆解成标量的。那么，JIT就会不会直接创建Point对象，而是直接使用两个标量int x ，int y来替代Point对象。

以上代码，经过标量替换后，就会变成
```java
private static void alloc() {
   int x = 1;
   int y = 2;
   System.out.println("point.x="+x+"; point.y="+y);
}
```
可以看到，Point这个聚合量经过逃逸分析后，发现他并没有逃逸，就被替换成两个聚合量了。那么标量替换有什么好处呢？就是可以大大减少堆内存的占用。因为一旦不需要创建对象了，那么就不再需要分配堆内存了。

标量替换为栈上分配提供了很好的基础。

## 栈上分配

在Java虚拟机中，对象是在Java堆中分配内存的，这是一个普遍的常识。但是，有一种特殊情况，那就是如果经过逃逸分析后发现，一个对象并没有逃逸出方法的话，那么就可能被优化成栈上分配。这样就无需在堆上分配内存，也无须进行垃圾回收了。

其实在现有的虚拟机中，并没有真正的实现栈上分配，对象没有在堆上分配其实是标量替换实现的。

[java逃逸分析参考博文](https://zhuanlan.zhihu.com/p/39604934)

[栈上分配实验博文](http://www.hollischuang.com/archives/2398)

# JVM调优  博文很好,有时间提炼

[参考博文](https://blog.csdn.net/RickyIT/article/details/53895060)

Xms 是指设定程序启动时占用内存大小。一般来讲，大点，程序会启动的快一点，但是也可能会导致机器暂时间变慢。

Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存，超出了这个设置值，就会抛出OutOfMemory异常。

Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序，看一个线程大约需要占用多少内存，可能会有多少线程同时运行等。

-Xmn2g ：设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小 。持久代一般固定大小为64m，所以增大年轻代后，将会减小年老代大小。此值对系统性能影响较大，Sun官方推荐配置为整个堆的3/8。

# Tomcat调优<file_sep>/notes/Golang.md

- [目录结构](#目录结构)
    - [make 和 new 的异同](#make-和-new-的异同)
        - [new](#new)
        - [make](#make)
- [基本语法](#基本语法)
- [HTTP](#http)
- [URL](#url)
    - [绝对url与相对url](#绝对url与相对url)
- [Curl](#curl)
- [ASCII 与 Unicode 与 UTF-8](#ascii-与-unicode-与-utf-8)
- [iota的使用](#iota的使用)
- [flag.Parse()](#flagparse)
    - [基本数据类型](#基本数据类型)
        - [字符](#字符)
        - [字符串](#字符串)
        - [基本数据的默认值](#基本数据的默认值)
        - [数据转换](#数据转换)
        - [基本数据类型和string的转化](#基本数据类型和string的转化)
        - [string  转基本数据类型](#string--转基本数据类型)
    - [闭包](#闭包)
    - [defer](#defer)
    - [string 常用函数](#string-常用函数)
    - [时间类](#时间类)
    - [错误处理机制](#错误处理机制)
    - [数组](#数组)
        - [数组注意的细节](#数组注意的细节)
    - [切片](#切片)
        - [切片的三种定义方式](#切片的三种定义方式)
        - [切片的传参](#切片的传参)
        - [切片的append](#切片的append)
    - [map](#map)
        - [map的 curd](#map的-curd)
        - [map的遍历](#map的遍历)
        - [map切片](#map切片)
    - [go语言的面向对象](#go语言的面向对象)
    - [结构体](#结构体)
        - [结构体是值类型](#结构体是值类型)
        - [结构体的三种初始化方式](#结构体的三种初始化方式)
    - [方法](#方法)
        - [需要方法的原因](#需要方法的原因)
        - [方法的传值与传地址](#方法的传值与传地址)
    - [工厂模式代替构造函数](#工厂模式代替构造函数)
    - [断言:](#断言)
    - [时区对应表:](#时区对应表)
    - [匿名字段](#匿名字段)
    - [接口](#接口)
    - [结构体初始化](#结构体初始化)
    - [flag解析命令行参数](#flag解析命令行参数)
    - [序列化与反序列化](#序列化与反序列化)
    - [单元测试](#单元测试)
    - [协程(goroutine)](#协程goroutine)
    - [通道](#通道)
        - [通道分类](#通道分类)
            - [只读  只写  双向](#只读--只写--双向)
        - [通道的阻塞情况](#通道的阻塞情况)
        - [for循环的阻塞情况](#for循环的阻塞情况)
        - [select一个线程读多个通道](#select一个线程读多个通道)
    - [反射](#反射)
    - [网络编程](#网络编程)
        - [tcp socket编程,比如qq聊天](#tcp-socket编程比如qq聊天)
        - [http编程,比如web开发](#http编程比如web开发)
    - [匿名函数](#匿名函数)
    - [go modules](#go-modules)
        - [go mod init example.com/hello](#go-mod-init-examplecomhello)
        - [import与gomod的细节](#import与gomod的细节)
        - [go list -m all](#go-list--m-all)
        - [包的版本号](#包的版本号)
            - [更改某个依赖包的版本号](#更改某个依赖包的版本号)
        - [常用命令](#常用命令)
    - [导入包的知识点](#导入包的知识点)
- [err的理解](#err的理解)
- [panic recover err](#panic-recover-err)
    - [panic](#panic)
    - [recover](#recover)
    - [defer](#defer-1)
    - [err和panic的区别](#err和panic的区别)
    - [总结](#总结)
    - [go build 和  go install的区别](#go-build-和--go-install的区别)
    - [proto的使用](#proto的使用)
    - [结构体的三种声明方式](#结构体的三种声明方式)
    - [解析json的方法](#解析json的方法)
        - [知道数据结构就先写数据结构](#知道数据结构就先写数据结构)
        - [不知道数据结构的时候解析json](#不知道数据结构的时候解析json)
    - [转型包 strconv  ParseXxx](#转型包-strconv--parsexxx)
    - [time](#time)
    - [go一个协程的原理,main怎么办](#go一个协程的原理main怎么办)
    - [http的知识](#http的知识)
        - [请求头](#请求头)
        - [请求行](#请求行)
        - [请求实体](#请求实体)
    - [go实现http请求](#go实现http请求)
        - [http.Get和http.Post](#httpget和httppost)
        - [http.Client](#httpclient)
    - [go部署到服务器](#go部署到服务器)
        - [linux服务器](#linux服务器)
        - [方法二：本地编译](#方法二本地编译)
    - [传文件到linux](#传文件到linux)
    - [nohup与&](#nohup与)
        - [nohup](#nohup)
        - [&](#)
        - [nohup与&区别](#nohup与区别)
    - [杀掉进程](#杀掉进程)
    - [遍历mysql某个表的所有行](#遍历mysql某个表的所有行)
    - [遍历结构体,切片,map](#遍历结构体切片map)
- [线上数据库的分批查询](#线上数据库的分批查询)
- [Cron表达式(定时格式)](#cron表达式定时格式)
    - [* 星号](#-星号)
    - [? 问号](#-问号)
    - [, 逗号](#-逗号)
    - [斜杠 (/)](#斜杠-)
    - [- 中划线](#--中划线)
    - [L 字母](#l-字母)
    - [W 字母](#w-字母)
    - [井号](#井号)
- [go 中while和do while的实现](#go-中while和do-while的实现)
    - [while](#while)
    - [do while](#do-while)
- [指针](#指针)
    - [重要知识点](#重要知识点)
    - [引用与指针区别](#引用与指针区别)
- [值类型与引用类型的区别](#值类型与引用类型的区别)
    - [声明变量时候的却别](#声明变量时候的却别)
    - [数值传递上的区别](#数值传递上的区别)
    - [在new 和 make的区别](#在new-和-make的区别)
        - [new](#new-1)
        - [make](#make-1)
        - [new 和 make的区别](#new-和-make的区别)
- [slice](#slice)
    - [介绍](#介绍)
    - [扩容](#扩容)
    - [nil切片和空切片](#nil切片和空切片)
    - [切片的三种定义方式,map两种](#切片的三种定义方式map两种)
        - [1.定义一个切片，然后让切片去引用一个已经创建好的数组](#1定义一个切片然后让切片去引用一个已经创建好的数组)
        - [2.通过make来创建切片。](#2通过make来创建切片)
        - [3.定义一个切片，直接就指定具体数组，使用原理类似于make的方式](#3定义一个切片直接就指定具体数组使用原理类似于make的方式)
        - [第一种和第二种的区别](#第一种和第二种的区别)
        - [共享数组产生bug](#共享数组产生bug)
- [map](#map-1)
    - [创建](#创建)
    - [底层原理](#底层原理)
    - [map的扩容](#map的扩容)
    - [加载因子](#加载因子)
- [channel](#channel)
    - [管道关闭原则](#管道关闭原则)
    - [select](#select)
- [协程](#协程)
    - [原理](#原理)
    - [改变线程多路复用的尴尬](#改变线程多路复用的尴尬)
    - [特点](#特点)
        - [内存占用少](#内存占用少)
        - [非抢占(非公平)](#非抢占非公平)
        - [切换开销小](#切换开销小)
    - [主从函数并发](#主从函数并发)
- [strings.NewReader()](#stringsnewreader)
    - [Len,Size，Read方法](#lensizeread方法)
- [wait group](#wait-group)
    - [WaitGroup对象不是一个引用类型](#waitgroup对象不是一个引用类型)
- [闭包](#闭包-1)
- [sql timestamp](#sql-timestamp)
- [Go Module](#go-module)
    - [go mod init](#go-mod-init)
    - [安装依赖](#安装依赖)
    - [移除依赖](#移除依赖)
    - [查看依赖包](#查看依赖包)
- [go build,install,get区别](#go-buildinstallget区别)
    - [go build](#go-build)
    - [go install](#go-install)
- [context  源码阅读](#context--源码阅读)
    - [简介](#简介)
    - [源码](#源码)
    - [context的根源上下文](#context的根源上下文)
    - [其他类型的context](#其他类型的context)
    - [WithCancel源码阅读](#withcancel源码阅读)
    - [golang的redis包](#golang的redis包)
        - [redigo模块的详细使用](#redigo模块的详细使用)
            - [连接](#连接)
            - [使用Pipelining操作redis](#使用pipelining操作redis)

## 目录结构

https://blog.csdn.net/stpeace/article/details/82710969
goroot  是 go的sdk的安装目录

gopath是项目的存储路径,gopath下有src,pkg,bin

src下是源代码

pkg是执行`go install <import下的依赖包名字> `时,在pkg下生成一份main中依赖的包

bin是执行`go install main.go` 时,在bin下生成可执行文件main,同时在pkg中生成依赖的包文件

## make 和 new 的异同

二者都是内存的分配（堆上），但是make只用于slice、map以及channel的初始化（非零值）；而new用于类型的内存分配，并且内存置为零。所以在我们编写程序的时候，就可以根据自己的需要很好的选择了。

make返回的还是这三个引用类型本身；而new返回的是指向类型的指针。

### new
```
package main
import (
 "fmt"
)
func main() {
 var i *int
 *i=10
 fmt.Println(*i)
}
这个例子会打印出什么？0还是10?。以上全错，运行的时候会painc，原因如下：
```
panic: runtime error: invalid memory address or nil pointer dereference
从这个提示中可以看出，对于引用类型的变量，我们不光要声明它，还要为它分配内容空间，否则我们的值放在哪里去呢？这就是上面错误提示的原因。

对于值类型的声明不需要，是因为已经默认帮我们分配好了。

对于上面的问题我们如何解决呢？既然我们知道了没有为其分配内存，那么我们使用new分配一个吧。
```
func main() {
 var i *int
 i=new(int)
 *i=10
 fmt.Println(*i)
}
```
现在再运行程序，完美PASS，打印10。现在让我们看下new这个内置的函数。

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
它只接受一个参数，这个参数是一个类型，分配好内存后，返回一个指向该类型内存地址的指针。同时请注意它同时把分配的内存置为零，也就是类型的零值。

我们的例子中，如果没有*i=10，那么打印的就是0。这里体现不出来new函数这种内存置为零的好处，我们再看一个例子。
```
func main() {
 u:=new(user)
 u.lock.Lock()
 u.name = "张三"
 u.lock.Unlock()
 fmt.Println(u)
}
type user struct {
 lock sync.Mutex
 name string
 age int
}
```
示例中的user类型中的lock字段我不用初始化，直接可以拿来用，不会有无效内存引用异常，因为它已经被零值了。

这就是new，它返回的永远是类型的指针，指向分配类型的内存地址。

### make

make也是用于内存分配的，但是和new不同，它只用于chan、map以及切片的内存创建，而且它返回的类型就是这三个类型本身，而不是他们的指针类型，因为这三种类型就是引用类型，所以就没有必要返回他们的指针了。

注意，因为这三种类型是引用类型，所以必须得初始化，但是不是置为零值，这个和new是不一样的。

func make(t Type, size ...IntegerType) Type
从函数声明中可以看到，返回的还是该类型

# 基本语法

引入的包或者变量没有用到,编译无法通过

推荐使用行注释而不是快注释

go的每一个文件都属于一个包,go是用包来管理文件的

func的首字母大写类似于其他语言的public

导包的时候从src下面开始写

build mian包的时候,其他的包会编译成.a文件,存放在pkg里面

切片

Go 数组的长度不可改变，在特定场景中这样的集合就不太适用，Go中提供了一种灵活，功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的，可以追加元素，在追加时可能使切片的容量增大。

直接初始化切片，[]表示是切片类型，{1,2,3}初始化值依次是1,2,3.其cap=len=3

s := arr[:]

初始化切片s,是数组arr的引用

断言:
```
value,ok: = a.(string)
if !ok{
    fmt.Println("不是string类型")
}
fmt.Println("值为",value)
```

# HTTP

golang 发送http请求:https://blog.csdn.net/iamlihongwei/article/details/78854899

# URL

URL 由多个部分组成。为了讲解，下面是一个比较复杂的 URL。

https://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#anchor

我们看看，这个 URL 的各个部分。

协议

协议（scheme）是浏览器请求服务器资源的方法，上例是https://的部分，表示使用 HTTPS 协议。

互联网支持多种协议，必须指明网址使用哪一种协议，默认是 HTTP 协议。也就是说，如果省略协议，直接在浏览器地址栏输入www.example.com，那么浏览器默认会访问http://www.example.com。HTTPS 是 HTTP 的加密版本，出于安全考虑，越来越多的网站使用这个协议。

HTTP 和 HTTPS 的协议名称后面，紧跟着一个冒号和两个斜杠（://）。其他协议不一定如此，邮件地址协议mailto:的协议名后面只有一个冒号，比如mailto:<EMAIL>。

主机

主机（host）是资源所在的网站名或服务器的名字，又称为域名。上例的主机是www.example.com。

有些主机没有域名，只有 IP 地址，比如192.168.2.15。这种情况常常出现在局域网。

端口

同一个域名下面可能同时包含多个网站，它们之间通过端口（port）区分。“端口”就是一个整数，可以简单理解成，访问者告诉服务器，想要访问哪一个网站。默认端口是80，如果省略了这个参数，服务器就会返回80端口的网站。

端口紧跟在域名后面，两者之间使用冒号分隔，比如www.example.com:80。

路径

路径（path）是资源在网站的位置。比如，/path/index.html这个路径，指向网站的/path子目录下面的网页文件index.html。

互联网的早期，路径是真实存在的物理位置。现在由于服务器可以模拟这些位置，所以路径只是虚拟位置。

路径可能只包含目录，不包含文件名，比如/foo/，甚至结尾的斜杠都可以省略。这时，服务器通常会默认跳转到该目录里面的index.html文件（即等同于请求/foo/index.html），但也可能有其他的处理（比如列出目录里面的所有文件），这取决于服务器的设置。一般来说，访问www.example.com这个网址，很可能返回的是网页文件www.example.com/index.html。

查询参数

查询参数（parameter）是提供给服务器的额外信息。参数的位置是在路径后面，两者之间使用?分隔，上例是?key1=value1&key2=value2。

查询参数可以有一组或多组。每组参数都是键值对（key-value pair）的形式，同时具有键名(key)和键值(value)，它们之间使用等号（=）连接。比如，key1=value就是一个键值对，key1是键名，value1是键值。

多组参数之间使用&连接，比如key1=value1&key2=value2。

锚点

锚点（anchor）是网页内部的定位点，使用#加上锚点名称，放在网址的最后，比如#anchor。浏览器加载页面以后，会自动滚动到锚点所在的位置。

## 绝对url与相对url
资源存放的相对路径与绝对路径

# Curl

发送表单信息
发送表单信息有GET和POST两种方法。GET方法相对简单，只要把数据附在网址后面即可。
```
$ curl www.cnblogs.com/form.cgi?data=xxx
```
POST方法必须把数据和网址分开，curl就要用到–data参数。
```
$ curl -X POST --data "data=xxx" www.cnblogs.com/form.cgi

curl -X POST $robot_addr -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"推荐索引构建告警：\n$content\"}"
```

# ASCII 与 Unicode 与 UTF-8

ASCII码
在学校学 C 语言的时候，了解到一些计算机内部的机制，知道所有的信息最终都表示为一个二进制的字符串，每一个二进制位有 0 和 1 两种状态，通过不同的排列组合，使用 0 和 1 就可以表示世界上所有的东西，感觉有点中国“太极”的感觉——“太极生两仪，两仪生四象，四象生八卦”。

在计算机种中，1 字节对应 8 位二进制数，而每位二进制数有 0、1 两种状态，因此 1 字节可以组合出 256 种状态。如果这 256 中状态每一个都对应一个符号，就能通过 1 字节的数据表示 256 个字符。美国人于是就制定了一套编码（其实就是个字典），描述英语中的字符和这 8 位二进制数的对应关系，这被称为 ASCII 码。

ASCII 码一共定义了 128 个字符，例如大写的字母 A 是 65（这是十进制数，对应二进制是0100 0001）。这 128 个字符只使用了 8 位二进制数中的后面 7 位，最前面的一位统一规定为 0。

历史问题
英语用 128 个字符来编码完全是足够的，但是用来表示其他语言，128 个字符是远远不够的。于是，一些欧洲的国家就决定，将 ASCII 码中闲置的最高位利用起来，这样一来就能表示 256 个字符。但是，这里又有了一个问题，那就是不同的国家的字符集可能不同，就算它们都能用 256 个字符表示全，但是同一个码点（也就是 8 位二进制数）表示的字符可能可能不同。例如，144 在阿拉伯人的 ASCII 码中是 گ，而在俄罗斯的 ASCII 码中是 ђ。

因此，ASCII 码的问题在于尽管所有人都在 0 - 127 号字符上达成了一致，但对于 128 - 255 号字符上却有很多种不同的解释。与此同时，亚洲语言有更多的字符需要被存储，一个字节已经不够用了。于是，人们开始使用两个字节来存储字符。

各种各样的编码方式成了系统开发者的噩梦，因为他们想把软件卖到国外。于是，他们提出了一个“内码表”的概念，可以切换到相应语言的一个内码表，这样才能显示相应语言的字母。在这种情况下，如果使用多语种，那么就需要频繁的在内码表内进行切换。

Unicode
最终，美国人意识到他们应该提出一种标准方案来展示世界上所有语言中的所有字符，出于这个目的，Unicode诞生了。

Unicode 当然是一本很厚的字典，记录着世界上所有字符对应的一个数字。具体是怎样的对应关系，又或者说是如何进行划分的，就不是我们考虑的问题了，我们只用知道 Unicode 给所有的字符指定了一个数字用来表示该字符。

对于 Unicode 有一些误解，它仅仅只是一个字符集，规定了符合对应的二进制代码，至于这个二进制代码如何存储则没有任何规定。它的想法很简单，就是为每个字符规定一个用来表示该字符的数字，仅此而已。

Unicode 编码方案
之前提到，Unicode 没有规定字符对应的二进制码如何存储。以汉字“汉”为例，它的 Unicode 码点是 0x6c49，对应的二进制数是 110110001001001，二进制数有 15 位，这也就说明了它至少需要 2 个字节来表示。可以想象，在 Unicode 字典中往后的字符可能就需要 3 个字节或者 4 个字节，甚至更多字节来表示了。

这就导致了一些问题，计算机怎么知道你这个 2 个字节表示的是一个字符，而不是分别表示两个字符呢？这里我们可能会想到，那就取个最大的，假如 Unicode 中最大的字符用 4 字节就可以表示了，那么我们就将所有的字符都用 4 个字节来表示，不够的就往前面补 0。这样确实可以解决编码问题，但是却造成了空间的极大浪费，如果是一个英文文档，那文件大小就大出了 3 倍，这显然是无法接受的。

于是，为了较好的解决 Unicode 的编码问题， UTF-8 和 UTF-16 两种当前比较流行的编码方式诞生了。当然还有一个 UTF-32 的编码方式，也就是上述那种定长编码，字符统一使用 4 个字节，虽然看似方便，但是却不如另外两种编码方式使用广泛。

UTF-8
UTF-8 是一个非常惊艳的编码方式，漂亮的实现了对 ASCII 码的向后兼容，以保证 Unicode 可以被大众接受。

UTF-8 是目前互联网上使用最广泛的一种 Unicode 编码方式，它的最大特点就是可变长。它可以使用 1 - 4 个字节表示一个字符，根据字符的不同变换长度。编码规则如下：

对于单个字节的字符，第一位设为 0，后面的 7 位对应这个字符的 Unicode 码点。因此，对于英文中的 0 - 127 号字符，与 ASCII 码完全相同。这意味着 ASCII 码那个年代的文档用 UTF-8 编码打开完全没有问题。

对于需要使用 N 个字节来表示的字符（N > 1），第一个字节的前 N 位都设为 1，第 N + 1 位设为0，剩余的 N - 1 个字节的前两位都设位 10，剩下的二进制位则使用这个字符的 Unicode 码点来填充。

根据上面编码规则对照表，进行 UTF-8 编码和解码就简单多了。下面以汉字“汉”为利，具体说明如何进行 UTF-8 编码和解码。

“汉”的 Unicode 码点是 0x6c49（110 1100 0100 1001），通过上面的对照表可以发现，0x0000 6c49 位于第三行的范围，那么得出其格式为 1110xxxx 10xxxxxx 10xxxxxx。接着，从“汉”的二进制数最后一位开始，从后向前依次填充对应格式中的 x，多出的 x 用 0 补上。这样，就得到了“汉”的 UTF-8 编码为 11100110 10110001 10001001，转换成十六进制就是 0xE6 0xB7 0x89。

解码的过程也十分简单：如果一个字节的第一位是 0 ，则说明这个字节对应一个字符；如果一个字节的第一位1，那么连续有多少个 1，就表示该字符占用多少个字节。

# iota的使用

https://www.jianshu.com/p/08d6a4216e96

# flag.Parse()
```go
import "fmt"

var infile *string = flag.String("i", "infile", "File contains values for sorting")
var outfile *string = flag.String("o", "outfile", "File to receive sorted values")
var algorithm *string = flag.String("a", "qsort", "Sort algorithm")

func main() {
	flag.Parse()
    if infile != nil {
		fmt.Println("infile =", *infile, "outfile =", *outfile, "algorithm =", *algorithm)
	}
}
```
flag.String()方法返回的是保存日后解析出来的对应参数的值的位置、位置、位置，是一个已经分配好的空间，我们可以用infile这个指针变量来接受这个位置，待日后重新进入main入口进呢人执行flag.Parse()函数之后，这3个位置上就出现了我们传入的参数（其实在程序初始化期就有默认值），可以使用*infile来访问具体的内容。

## 基本数据类型

### 字符

### 字符串

反引号

统一unicode

赋值后不可修改

### 基本数据的默认值

0  ""  和  flase

### 数据转换

不能编译器自动转换,要显示转换

var a int32 = int32(b);b的值转换成int32给了a,**但b本身的数据类型和值没有改变**

int8转换成int64 不报错,但按照溢出结果显示

### 基本数据类型和string的转化
方法一  使用fmt
```
用Sprintf转换
var a int32 = 13
var b float32 = 12.222
fmt.Sprintf("%d",a)
fmt.Sprintf("%f",b)
```

方法二  strconv类

### string  转基本数据类型

strconv.ParseInt/Bool

前提是string有效 不能是"hello"之类的非数字字符串

## 闭包

函数引用的参数和 函数本身构成闭包

## defer

## string 常用函数
包括字符串操作,int类型和进制的转换

## 时间类

time.Now()

Format的20060102的知识

时间单位  time.Second  time.MillionSecond

## 错误处理机制

errors.New("错误信息")

panic()  接受interface{}类型的参数,可以接受error类型变量,输出错误信息,并**退出程序**

recover(),用defer压栈 可以捕获异常,见下图

## 数组

var array [2]int = [2]int {1,2}

array:=[...]int{1,2,3,4}

数组的内存是连续存放的

遍历用 for index,value:= range array{}

### 数组注意的细节

1) 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化

2)var arr []int 这时 arr 就是一个 slice 切片

1) 数组中的元素可以是任何数据类型，包括值类型和引用类型，但是不能混用。

2) 数组创建后，如果没有赋值，有默认值(零值)
数值类型数组:默认值为 0 字符串数组: 默认值为 "" bool 数组: 默认值为 false

1) 数组下标必须在指定范围内使用，否则报 panic:数组越界，比如 var arr [5]int 则有效下标为 0-4

2) **Go的数组属值类型，在默认情况下是值传递，因此会进行值拷贝。数组间不会相互影响**

3) 如想在其它函数中，去修改原来的数组，可以使用引用传递(指针方式)

4)  长度是数组类型的一部分，在传递函数参数时 需要考虑数组的长度

## 切片

切片

Go 数组的长度不可改变，在特定场景中这样的集合就不太适用，Go中提供了一种灵活，功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的，可以追加元素，在追加时可能使切片的容量增大。

直接初始化切片，[]表示是切片类型，{1,2,3}初始化值依次是1,2,3.其cap=len=3

s := arr[:]

初始化切片s,是数组arr的引用

切片的长度是可以变化的，因此切片是一个可以动态变化数组。

切片定义的基本语法:
  var 切片名 []类型 比如:var a [] int

slice 是一个引用类型

slice 从底层来说，其实就是一个数据结构(struct 结构体)
```
type slice struct {
ptr *[2]int
len int
cap int
}
```

### 切片的三种定义方式
定义一个切片,然后定义一个数组,并把数组的一部分分给切片

make  指定大小和容量,底层管理数组

定义切片的时候直接 初始化一个数组

append()  底层创建一个新的数组,对原数组进行拷贝

切片是引用类型,遵循引用的规律

切片由大传小的时候不会报错,而是把对应位置的赋值过去

string 是一个底层数组是byte的切片,可以使用相关的切片功能,但是string是不可修改的,可以先转换成byte数组,修改,再转换成string
### 切片的传参
```go
func main() {

    slice := []int{0, 1, 2, 3}
    fmt.Printf("slice: %v slice addr %p \n", slice, &slice)

    ret := changeSlice(slice)
    fmt.Printf("slice: %v ret: %v slice addr %p \n", slice, &slice, ret)
}

func changeSlice(s []int) []int {
	fmt.Printf("func: %p \n", &s)
    s[1] = 111
    return s
}
```
输出
```
slice: [0 1 2 3], slice addr: 0xc4200660c0
func: 0xc4200661c0 (与main函数中的地址不同)
slice: [0 111 2 3], ret: [0 111 2 3], slice addr: 0xc4200660c0
```

问题1:
slice 和 &slice打印的结果都是数据,&slice没有打印地址
```
Printf("%v", people)   {zhangsan}，
Printf("%+v", people)  {Name:zhangsan}
Printf("#v", people)   main.Human{Name:"zhangsan"}
Printf("%T", people)   main.Human
```
问题2:
slice作为传参,在changeslice函数内部修改后,main里面的slice也发生了变化

因为slice被传入时,他的参数被复制了一份给changeslice内部的s,s和slice指向的是同一个底层匿名数组,所以修改s,本质是修改底层数组.slice的底层就被修改了

问题3:
changeslice函数内部的slice和main里面的slice不是同一个

因为slice被传入时,他的参数被复制了一份给changeslice内部的s,所以s和slice其实是两个不同的变量,所以s的地址与slice地址不同

问题4:
如果保证s和slice的地址相同?

传引用:
```go
func main() {
    slice := []int{0, 1}
    fmt.Printf("slice %v %p \n", slice, &slice)

    changeSlice(&slice)
    fmt.Printf("slice %v %p \n", slice, &slice)

    slice[1] = -1111

    fmt.Printf("slice %v %p \n", slice, &slice)
}

func changeSlice(s *[]int) {
    fmt.Printf("func s %v %p \n", *s, s)
    (*s)[0] = -1
    *s = append(*s, 3)
    (*s)[1] =  1111
}

输出

slice [0 1] 0xc42000a1e0
func s [0 1] 0xc42000a1e0
slice [-1 1111 3] 0xc42000a1e0
slice [-1 -1111 3] 0xc42000a1e0
```

### 切片的append
切片都依赖底层的数组结构，即使是直接创建的切片，也会生成一个匿名的数组。

使用append时候，本质上是针对底层依赖的数组进行操作。如果切片的容量大于长度，给切片追加元素其实是修改底层数中，切片元素后面的元素。

如果容量满了，就不能在原来的数组上修改，而是要创建一个新的数组，当然golang是通过创建一个新的切片实现的，因为新切片必然也有一个新的数组，并且这个数组的长度是原来的2倍，使用动态规划算法的简单实现。

超出容量的append过程:

1.创建一个新的临时切片t，t的长度和slice切片的长度一样，但是t的容量是slice切片的2倍，一个动态规划的方式。新建切片的时候，底层也创建了一个匿名的数组，数组的长度和切片容量一样。

2.复制s里面的元素到t里，即填入匿名数组中。然后把t赋值给slice，现在slice的指向了底层的匿名数组。

3.转变成小于容量的append方法。

## map

声明不会赋值,make的时候分配了内存空间,才能赋值和使用
声明方式1:
var m map[string] string
m = make(map[string]string,10)

声明方式2

var m = make(map[string]string,10)

声明方式3

var m map[string][string] = map[string]string{"no1":"sx"}

### map的 curd

m["no1"]="zyc"

delete (m,"no1")

批量删除: make一个新空间

### map的遍历

没啥难度,用到再查

### map切片

一个切片,每一个下标的内容都是一个逻辑实体的几个key,value ;比如:
[妖怪一: 年龄:13 名字:sx][妖怪二:  年龄:18  名字:lll]

好处是可以让key,value对用append()动态增长;并且每一个下标内容都可以代表一个逻辑实体

```
var sm []map[string]string = []map[string]string
sm[0] = make(map[string]string){"年龄":"12","名字":"sx"}
m2 = make(map[string]string){"年龄":"18","名字":"lll"}
sm.append(m2)
```

## go语言的面向对象

去除了继承,重载,构造函数,析构函数,隐藏this指针

go的类是 struct,继承通过匿名字段来实现,

go语言其实是面向接口编程

## 结构体
### 结构体是值类型
值类型不需要new一个对象就会分配内存

结构体类型是值类型,拷贝的时候是值拷贝,p2=p1的时候 p1的空间会被拷贝一份;如果修改p1的内容,p2内容不变,如果 var p2 *Person = &p1 这个时候拷贝的不再是内存,而是空间地址,p1,p2指向的是同一个对象

值类型在声明的时候,内部的成员变量就有初始值,赋值不会出现空指针异常

值类型的内存分配是在栈上的

go语言中,指向结构体实例的变量,是一个引用类型;有的语言是指针类型,比如 :
cat1:= struct{},cat1是一个引用类型

结构体内存是连续的

结构体的tag标签

### 结构体的三种初始化方式
```go
// 1-struct as a value type:
    var pers1 Person
    pers1.firstName = "Chris"
    pers1.lastName = "Woodward"
    upPerson(&pers1)
    fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)

    // 2—struct as a pointer:
    pers2 := new(Person)
    pers2.firstName = "Chris"
    pers2.lastName = "Woodward"
    (*pers2).lastName = "Woodward"  // 这是合法的
    upPerson(pers2)
    fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)

    // 3—struct as a literal:
    pers3 := &Person{"Chris","Woodward"}
    upPerson(pers3)
    fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}
```

## 方法

### 需要方法的原因

Go 不是纯粹的面向对象编程语言，而且Go不支持类。因此，基于类型的方法是一种实现和类相似行为的途径。

相同的名字的方法可以定义在不同的类型上，而相同名字的函数是不被允许的。假设我们有一个 Square 和 Circle 结构体。可以在 Square 和 Circle 上分别定义一个 Area 方法。

### 方法的传值与传地址

在func与方法名之间,可以使用指针接收器与值接收器,该方法就可以使用结构体的字段

一般来说，指针接收器可以使用在：对方法内部的接收器所做的改变应该对调用者可见时。

指针接收器也可以被使用在如下场景：当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收器需要拷贝整个结构体，这是很昂贵的。在这种情况下使用指针接收器，结构体不会被拷贝，只会传递一个指针到方法内部使用。

在其他的所有情况，值接收器都可以被使用。

区分func后面是传值还是传地址
```go
//对于方法（如struct的方法），
//接收者为值类型时，可以直接用指针类型的变量调用方法，反过来同样也可以

func (p Person) test03() {
	p.Name = "jack"
	fmt.Println("test03() =", p.Name) // jack
}

func (p *Person) test04() {
	p.Name = "mary"
	fmt.Println("test03() =", p.Name) // mary
}

func main() {

	p := Person{"tom"}
	test01(p)
	test02(&p)

	p.test03()
	fmt.Println("main() p.name=", p.Name) // tom

	(&p).test03() // 从形式上是传入地址，但是本质仍然是值拷贝

	fmt.Println("main() p.name=", p.Name) // tom

	(&p).test04()
	fmt.Println("main() p.name=", p.Name) // mary
	p.test04() // 等价 (&p).test04 , 从形式上是传入值类型，但是本质仍然是地址拷贝

}
```
## 工厂模式代替构造函数

## 断言:
```
value,ok: = a.(string)
if !ok{
    fmt.Println("不是string类型")
}
fmt.Println("值为",value)
```

## 时区对应表:

```
月份 1,01,Jan,January
日　 2,02,_2
时　 3,03,15,PM,pm,AM,am
分　 4,04
秒　 5,05
年　 06,2006
时区 -07,-0700,Z0700,Z07:00,-07:00,MST
周几 Mon,Monday
```
```
format := time.Now().Format("2006年01月15时 时区MST")
fmt.Println(format)
```

## 匿名字段

go通过匿名来实现继承

当匿名字段是一个struct的时候，那么这个struct所拥有的全部字段,方法,无论大写小写都可以被使用

可以使用简化访问   A继承了B   a.b.name  可以简化为  a.name

如果继承了多个结构体,有相同的字段,则不能用简化的方式

## 接口

接口不包含变量,但是可以包含接口(继承)

go中没有implements关键字,只要一个变量包含了接口的所有方法,就说实现了该方法

自定义数据类型 和  结构体都可以实现接口

一个类型需要把接口所有的方法都实现

A接口继承了B C接口  ,如果要实现A接口,那BC接口的方法也要实现

接口是一个指针(引用类型),所以如果不初始化就使用,会返回nil

## 结构体初始化

rect1 := new(Rect)

rect3 := &Rect{0, 0, 100, 200}

rect4 := &Rect{width:100, height:200}

rect2 := &Rect{}

```
注意以上四个全部为指向Rect结构的指针(指针变量)，因为使用了new()函数和&操作符,指针有了值之后,它输出的就是指针所指向值得类型．

a := Rect{}则表示这个是一个Rect{}类型的指针

输出类型%T时候的区别:

*main.Rect()  //前四个

main.Rect
```

## flag解析命令行参数

``` go

func main() {

	//定义几个变量，用于接收命令行的参数值
	var user string
	var pwd string
	var host string
	var port int

	//&user 就是接收用户命令行中输入的 -u 后面的参数值
	//"u" ,就是 -u 指定参数
	//"" , 默认值
	//"用户名,默认为空" 说明
	flag.StringVar(&user, "u", "", "用户名,默认为空")
	flag.StringVar(&pwd, "pwd", "", "密码,默认为空")
	flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost")
	flag.IntVar(&port, "port", 3306, "端口号，默认为3306")
	//这里有一个非常重要的操作,转换， 必须调用该方法
	flag.Parse()

	//输出结果
	fmt.Printf("user=%v pwd=%v host=%v port=%v",
		user, pwd, host, port)

}
```

## 序列化与反序列化

序列化
```go
	//将monster 序列化
	data, err := json.Marshal(&monster) //..
	if err != nil {
		fmt.Printf("序列号错误 err=%v\n", err)
	}
	//输出序列化后的结果
	fmt.Printf("monster序列化后=%v\n", string(data))
```

反序列化

```go

//演示将json字符串，反序列化成map
func unmarshalMap() {
	//str := "{\"address\":\"洪崖洞\",\"age\":30,\"name\":\"红孩儿\"}"
	str := testMap()
	//定义一个map
	var a map[string]interface{}

	//反序列化
	//注意：反序列化map,不需要make,因为make操作被封装到 Unmarshal函数
	//传入的是引用传递,只有引用传递才会改变函数外面的a的值
	err := json.Unmarshal([]byte(str), &a)
	if err != nil {
		fmt.Printf("unmarshal err=%v\n", err)
	}
	fmt.Printf("反序列化后 a=%v\n", a)

}
```

## 单元测试

## 协程(goroutine)

go的主线程(进程级别)内,可以开启多个协程(编译器级别优化,轻量级的线程)

协程特点:
有独立的栈空间
共享程序堆内存
调度由用户控制
协程是轻量级的线程

协程的资源消耗要小于线程,这是go语言的在并发上的巨大优势.线程等于堆资源+栈 ,协程是多个协程存储在一个线程中,协程的堆资源在线程中,栈资源用线程的堆来保存.所以多个协程可以在一个线程里面运行.

MPG模式

m是主线程,p是上下文,g是协程

## 通道

先进先出,线程安全,存储类型统一

管道关闭后,只可读,不可写

遍历用for-range,遍历的前提是管道关闭,不然报错

```go
管道使用实例

//演示一下管道的使用
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan<- 10
num := 211
intChan<- num
intChan<- 50
// //如果从channel取出数据后，可以继续放入
<-intChan
intChan<- 98//注意点, 当我们给管写入数据时，不能超过其容量
//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
//5. 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))  // 2, 3

管道的遍历
for v:=range intChan{
	fmt.Println(v)
}
```

管道读写的实例
```go

//write Data
func writeData(intChan chan int) {
	for i := 1; i <= 50; i++ {
		//放入数据
		intChan<- i //
		fmt.Println("writeData ", i)
		//time.Sleep(time.Second)
	}
	close(intChan) //关闭
}

//read data
func readData(intChan chan int, exitChan chan bool) {

	for {
		v, ok := <-intChan
		if !ok {
			break
		}
		time.Sleep(time.Second)
		fmt.Printf("readData 读到数据=%v\n", v)
	}
	//readData 读取完数据后，即任务完成
	exitChan<- true
	close(exitChan)

}

func main() {

	//创建两个管道
	intChan := make(chan int, 10)
	exitChan := make(chan bool, 1)

	go writeData(intChan)
	go readData(intChan, exitChan)

	//time.Sleep(time.Second * 10)
	//exitChan存在的意义是为了防止主线程main的退出,要等到readData读完之后再退出
	for {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}

}
```
### 通道分类

#### 只读  只写  双向
只读:

var chan1 <-chan int

 only_read := make(<-chan int,1)

只写

var chan2 chan<- int

only_write := make(chan<- int, 1)

双向通道

make (chan int , 10)

在调用只接受只读/只写通道的函数的时候，只需要把一个元素类型匹配的双向通道传给它就行了。没必要用单向通道，因为Go 语言在这种情况下自动地把双向通道转换为函数所需的单向通道。
比如:
```go
func SendInt(ch chan<- int) {
      ch <- rand.Intn(1000)
  }

intChan1 := make(chan int, 3)
  SendInt(intChan1)

```
### 通道的阻塞情况
无论是有缓存通道、还是无缓冲通道都存在阻塞的情况。阻塞场景共4个，有缓存和无缓冲各2个。

**无缓冲通道**的特点是，发送的数据需要被读取后发送才会完成（同步），它阻塞场景是：

通道中无数据，但执行读通道。
通道中无数据，向通道写数据，但无协程读取。

**有缓存通道**的特点是，有缓存时可以向通道中写入数据后直接返回（异步），它阻塞场景是：

通道缓存无数据，但执行读通道(接收数据)。
通道缓存已经占满，向通道写数据(发送数据)，但无协程读。

### for循环的阻塞情况
for range 遍历通道要注意的点：

  1.for 语句会不断尝试从intChan2 中取出元素值，即使intChan2被关闭，它也会在取出所有剩余的元素值之后再结束关闭。
  2.当 intChan2中没有元素值时，它会被阻塞在有 for关键字的那一行，直捣有新的元素可取。
  3.假设intChan2的值为nil，那么它会被永远阻塞在有for关键字的那一行。

### select一个线程读多个通道

[select细节好文](https://studygolang.com/articles/15195)

```go
package main
import (
	"fmt"
	"time"
)

func main() {

	//使用select可以解决从管道取数据的阻塞问题

	//1.定义一个管道 10个数据int
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan<- i
	}
	//2.定义一个管道 5个数据string
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}

	//传统的方法在遍历管道时，如果不关闭会阻塞而导致 deadlock

	//问题，在实际开发中，可能我们不好确定什么关闭该管道.
	//可以使用select 方式可以解决
	//label:
	for {
		select {
			//注意: 这里，如果intChan一直没有关闭，不会一直阻塞而deadlock
			//，会自动到下一个case匹配
			case v := <-intChan :
				fmt.Printf("从intChan读取的数据%d\n", v)
				time.Sleep(time.Second)
			case v := <-stringChan :
				fmt.Printf("从stringChan读取的数据%s\n", v)
				time.Sleep(time.Second)
			default :
				fmt.Printf("都取不到了，不玩了, 程序员可以加入逻辑\n")
				time.Sleep(time.Second)
				return
				//break label
		}
	}
}
```

goroutine(协程)中使用recover,来解决协程中出现panic的问题,导致程序崩溃.使用了recover之后,出错协程的异常被捕获,主线程不受影响
```go

//函数
func sayHello() {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		fmt.Println("hello,world")
	}
}
//函数
func test() {
	//这里我们可以使用defer + recover
	defer func() {
		//捕获test抛出的panic
		if err := recover(); err != nil {
			fmt.Println("test() 发生错误", err)
		}
	}()
	//定义了一个map,这里有error
	var myMap map[int]string
	myMap[0] = "golang" //error
}

func main() {

	go sayHello()
	go test()

	for i := 0; i < 10; i++ {
		fmt.Println("main() ok=", i)
		time.Sleep(time.Second)
	}

}
```
## 反射

## 网络编程

### tcp socket编程,比如qq聊天

### http编程,比如web开发

## 匿名函数

defer的调用解读

```go
defer func() {
	 fmt.Println(“Hello，world”)
}()

可以看成

func aa() {
    fmt.Println("Hello，world")
}

func main() {

    defer aa()
}
```

## go modules

### go mod init example.com/hello

go.mod 文件只存在于模块的根目录中。模块子目录的代码包的导入路径等于模块根目录的导入路径（就是前面说的 module path）加上子目录的相对路径。比如，我们如果创建了一个子目录叫 world，我们不需要（也不会想要）在子目录里面再运行一次 go mod init 了，这个代码包会被认为就是 example.com/hello 模块的一部分，而这个代码包的导入路径就是 example.com/hello/world。

### import与gomod的细节
当我们imort一个包的时候, go.mod 文件里面又没有指定这个包的时候，go 命令行工具会自动寻找包含这个代码包的模块的最新版本，并添加到 go.mod 中（这里的 " 最新 " 指的是：它是最近一次被 tag 的稳定版本（即非预发布版本，non-prerelease），如果没有，则是最近一次被 tag 的预发布版本，如果没有，则是最新的没有被 tag 过的版本）。

比如，go新导入的 rsc.io/quote 包解析为 rec.io/quote v1.5.2模块:
```go
package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()

```

它还会下载 rsc.io/quote 模块依赖的两个依赖项。即 rsc.io/sampler 和 golang.org/x/text。但是只有直接依赖会记录在 go.mod 文件里面：

### go list -m all

在上述 go list 命令的输出中，当前的模块，又称为主模块 (main module)，永远都在第一行，接着是主模块的依赖项，以依赖项的 module path 排序。

### 包的版本号

一个语义化版本号包括三个部分：主版本号（major）、次版本号（minor）、修订号（patch）。举个例子：对于版本 v0.1.2，主版本号是 0，次版本号是 1，修订号是 2。我们先来过一遍更新某个模块的次版本号的流程。

#### 更改某个依赖包的版本号
```
go get rsc.io/sampler@v1.3.1

```

### 常用命令
```
go mod init 创建了一个新的模块，初始化 go.mod 文件并且生成相应的描述
go build, go test 和其它构建代码包的命令，会在需要的时候在 go.mod 文件中添加新的依赖项
go list -m all 列出了当前模块所有的依赖项
go get 修改指定依赖项的版本（或者添加一个新的依赖项）
go mod tidy 移除模块中没有用到的依赖项。
```

## 导入包的知识点

1. 点操作
我们有时候会看到如下的方式导入包
import(
. "fmt"
)
这个点操作的含义就是这个包导入之后在你调用这个包的函数时，你可以省略前缀的包名，也就是前面你调
用的fmt.Println("hello world")可以省略的写成Println("hello world")
2. 别名操作
别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字
import(
f "fmt"
)
别名操作的话调用包函数时前缀变成了我们的前缀，即f.Println("hello world")
3. _操作
这个操作经常是让很多人费解的一个操作符，请看下面这个import
import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)
_操作其实是引入该包，而不直接使用包里面的函数，而是调用了该包里面的init函数。

# err的理解

```go
for{
	err := rows.Scan(&tmp)
		if err != nil {
			log.Errorc(ctx, "mysql,rows.Scan failed,err(%s)", err)
			continue
		}
		mids = append(mids, tmp)
}
此处err如果存在,则不能再对mids进行append,所以不单单是log.Errorc,还要continue,不要把错误放进mids
```

```go
_, err := d.userClient.UpdateUserStatistics(ctx, in)
	if err != nil {
￼
		log.Errorc(ctx, "mysql,update user_statistic failed,err(%s)", err)
		return

	}
	如果err存在,则要return,让这个方法返回.不然后面的代码可能会出现不可预知的错误,导致程序崩溃
```

```go
func (d *Dao) GetVideoStatisticPlaySumByMid(ctx context.Context, mid int64) (sum int64,err error) {
	s := []int64{mid}
	in := &archive.FilterVideosReq{Mids: s}
	response, err := d.archiveClient.FilterVideos(ctx, in)
		reply, err := d.archiveClient.ListVideoStat(ctx, &req)
		if err != nil {
			log.Errorc(ctx, "mysql,list videostat by mid from video_statistic failed,err(%s)", err)
			// return 错误写法
			return -1,err
		}
		stat := reply.VideoStats[v.Svid].View
		sum += stat
	}
	// return sum  错误写法
	return sum,nil
}
这个函数的功能是获取mids,如果出现了err,要告知调用它的函数,所以要在返回值里面放一个err
```

# panic recover err

背景：Go语言追求简洁优雅，所以，Go语言不支持传统的 try…catch…finally 这种异常，因为Go语言的设计者们认为，将异常与控制结构混在一起会很容易使得代码变得混乱。因为开发者很容易滥用异常，甚至一个小小的错误都抛出一个异常。在Go语言中，使用多值返回来返回错误。不要用异常代替错误，更不要用来控制流程。在极个别的情况下，才使用Go中引入的Exception处理：defer, panic, recover。

## panic

假如函数F中发生了panic语句，会终止其后要执行的代码，在panic所在函数F内如果存在要执行的defer函数列表，按照defer的逆序执行

返回函数F的调用者G，在G中，调用函数F语句之后的代码不会执行，假如函数G中存在要执行的defer函数列表，按照defer的逆序执行，这里的defer 有点类似 try-catch-finally 中的 finally

直到goroutine整个退出，主程序崩溃并报告错误

## recover
recover专用于恢复panic,或者说平息运行时恐慌， recover 函数无需任何参数，并且会返回一个空接口类型的值

如果用法正确，这个值实际就是即将恢复的panic包含的值，并且，如果这个panic 是因为我们调用panic函数引发的，那么该值同时也会是我们此次调用panic函数时，传入的参数值副本
```go
package main

import (
    "fmt"
    "errors"
)

func main() {
    fmt.Println("Enter function main.")
    defer func() {
        fmt.Println("Enter defer function")
        if p:= recover(); p!= nil {
            fmt.Printf("panic :%s\n", p)
        }
        fmt.Println("Exit defer function")
    }()

    panic(errors.New("something wrong"))
    fmt.Println("Exit function main.")
}
```

## defer
defer 就是被用来延迟执行代码的。这个延迟会延迟到该语句所在的函数即将执行结束的那一刻，无论结执行的原因是什么

一个defer语句总是由一个defer关键字和一个调用表达式组成

但是需要注意一些限制： 有些表达式不能出现在这里，包括：针对Go语言内建函数的调用表达式，以及针对unsafe包中函数的调用表达式

## err和panic的区别
panic 表示进程内的错误。panic 的原因来自于代码的逻辑 bug，比如强制类型转换失败，比如数组越界。这个代表了程序员的责任不到位，导致了程序的panic。

error 代表进程外的错误。比如输入符合预期。比如访问外部的服务失败。这些都不是程序员可以设计控制的。这些情况的错误处理是业务逻辑的一部分。

## 总结
**error**

是一个用于标准错误处理的接口，是内建的接口类型

type error interface{

    Error() string

}

提供了New方法创建error结构体

Errorf函数

fmt中有一个返回error结构体的方法Errorf，内部使用errors包的New方法实现

**panic**

当遇到不可恢复的错误状态的时候，如数组访问越界、空指针引用等，这些运行时错误会引起panic异常。

创建panic

panic内置函数支持任何参数值

func panic(v interface{})

**recover**

recover用于从panic状态中恢复并重新获得流程控制权

func recover() interface{}

recover函数只在defer调用的函数中有效

defer func(){

    if err:= recover();err !=nil{

        fmt.Println(rcover())

    }

}

## go build 和  go install的区别

作用

go build：用于测试编译包，在项目目录下生成可执行文件（有main包）。

go install：主要用来生成库和工具。一是编译包文件（无main包），将编译后的包文件放到 pkg 目录下（$GOPATH/pkg）。二是编译生成可执行文件（有main包），将可执行文件放到 bin 目录（$GOPATH/bin）。

2. 相同点
都能生成可执行文件

1. 不同点
go build 不能生成包文件, go install 可以生成包文件
go build 生成可执行文件在当前目录下， go install 生成可执行文件在bin目录下（$GOPATH/bin）

## proto的使用

https://blog.csdn.net/DinnerHowe/article/details/79805250

## 结构体的三种声明方式

// 然后创建实例化结构体 : 每个元素都需要使用逗号 ，结尾
	// 简短变量创建
	m := Movie{
		Name:   "www.ydook.com",
		Rating: 10,
	}
	fmt.Println(m)

	// 显式创建
	var M Movie
	M.Name = "MyName"
	M.Rating = 9
	fmt.Println(M)

	// new() 简短变量创建
	movi := new(Movie)
	movi.Name = "New() struct"
	movi.Rating = 8
	fmt.Println(movi)
————————————————
版权声明：本文为CSDN博主「YDOOK」的原创文章，遵循 CC 4.0 BY-SA 版权协议，转载请附上原文出处链接及本声明。
原文链接：https://blog.csdn.net/weixin_42255190/article/details/98942901

## 解析json的方法

### 知道数据结构就先写数据结构
```go
package main
import (
	"bytes"
	"encoding/json"
	"fmt"
	"path/filepath"
	"regexp"
	"runtime"
)
type Contact struct {
	Name string `json:"name"`
	Title string `json:"title,omitempty"`
	Contact struct {
		Home string `json:"home"`
		Cell string `json:"cell"`
	} `json:"contact"`
}

var JSON = `{
	"name":"Gopher",
	"title":"",
	"contact":{
		"home":"123123123",
		"cell":"456456456"
		}
	}`

func main() {
	// 解析json
	var c Contact
	err := json.Unmarshal([]byte(JSON), &c)
	if err != nil {
		fmt.Println("ERROR", err)
		return
	}
	fmt.Println(c.Contact)
	fmt.Println(c.Title)
	fmt.Println(c.Name)

	// 编码json
	data, _ := json.Marshal(c)
	fmt.Println(string(data))
```
### 不知道数据结构的时候解析json
```go

	var tmp map[string]interface{}
	_ = json.Unmarshal([]byte(JSON), &tmp)

	//断言转型
	str := tmp["name"].(string)
	//显式转型(go没有自动转型)
	str2 : = string(tmp["name"])

}

```
## 转型包 strconv  ParseXxx

https://www.cnblogs.com/f-ck-need-u/p/9863915.html

## time

获取当前时间

currentTime:=time.Now()

进程休眠

time.Sleep(10 * time.Millisecond) //time的sleep

## go一个协程的原理,main怎么办

## http的知识
### 请求头
响应码是哪部分
### 请求行

### 请求实体

## go实现http请求

### http.Get和http.Post

```go
func httpGet() {
    resp, err := http.Get("http://www.baidu.com")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

func httpPost() {
    resp, err := http.Post("http://www.baidu.com",
                           "application/x-www-form-urlencode",
						   strings.NewReader("name=abc"))
	// Content-Type post请求必须设置,application/json比较常用
    if err != nil {
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}
```
### http.Client
```go
// http.Client
func httpDo() {
	client := &http.Client{}
	//设置请求头(方法和地址)和请求体
    req, err := http.NewRequest("POST",
                                "http://www.baidu.com",
                                strings.NewReader("name=abc"))
    if err != nil {
        return
	}
	//担任设置需要的请求行(属性的key和value)
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    resp, err := client.Do(req)
    if err != nil {
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return
    }
    fmt.Println(string(body))
}
```

## go部署到服务器

windows服务器
先本地编译

go build main.go

编译后会在同级目录生成可执行文件

main.exe

### linux服务器
方法一：服务器上去编译
安装go，配置GOPATH

1，把服务器上配置成本地一样的环境

2，包括项目里面所依赖的包

3，项目源码拷贝到与服务器，服务器上编译

go build main.go

编译后会在同级目录生成可执行文件

main

最后执行 ./main 就行了。

如果想让项目在后台执行：执行 nohup ./main & ，这样就可以程序在后台运行了

注意：发现如果按照官方给的安装方法只指定GOROOT的路径，于是自己新建一个目录gopath 作为GOPATH 的目录，并且设置环境变量（export GOPATH=/newhome/go/gopath）。在gopath下新建3个文件夹（模仿windows GOPATH的方式，也是go语言的工作环境目录）分别为 src、pkg、bin目录

### 方法二：本地编译
cmd控制台到main.go文件目录下

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build set.go

会生成一个没有后缀的二进制文件

main

将该文件放入linux系统某个文件夹下

赋予权限

chmod 777 main

最后执行 ./main 就行了。

如果想让项目在后台执行：执行 nohup ./main & ，这样就可以程序在后台运行了
## 传文件到linux

scp 文件路径  目标机器用户名@目标机器id:目标路径(比如~/)

## nohup与&

### nohup
不挂断地运行命令。我们在使用Xshell等工具执行Linux脚本时，有时候会由于网络问题，导致失去连接，终端断开，程序运行一半就意外结束了。这种时候，就可以用nohup指令来运行指令，使程序可以忽略挂起信号继续运行。

### &
用途：在后台运行

### nohup与&区别
&：是指在后台运行，当用户退出（挂起）的时候，命令自动跟着结束

nohup：不挂断的运行，注意并没有后台运行的功能，就是指用nohup运行命令可以使命令永久的执行下去，和用户终端没有关系，例如我们断开SSH连接都不会影响他的运行，注意了nohup没有后台运行的意思；&才是后台运行

因此将nohup和&结合使用，就可以实现使命令永久地在后台执行的功能

## 杀掉进程

netstat -antp | grep 端口号

ps -ef | grep  程序名

kill -9 pid

## 遍历mysql某个表的所有行
```go
rows, err := d.db.Query(ctx, _getAllMid)
	defer rows.Close()
	var tmp int64
	if err!=nil{
		log.Error("")
	}
	for rows.Next() {
		err:=rows.Scan(&tmp)
		if err!=nil{
			log.Error("")
		}
		mids = append(mids, tmp)
	}
```
## 遍历结构体,切片,map
```go
package main
import (
    "fmt"
    "reflect"
)
type person struct {
    name string
    age  int
}
func main() {
    v := reflect.ValueOf(person{"steve", 30})
    count := v.NumField()
    for i := 0; i < count; i++ {
        f := v.Field(i)
        switch f.Kind() {
        case reflect.String:
            fmt.Println(f.String())
        case reflect.Int:
            fmt.Println(f.Int())
        }
    }
}
```

```go
切片
slice := []string{"hello","world","hello","everyone!"}
    for k,val:=range slice{
        fmt.Printf("slice %d is :%s\n",k,val )
	}
```

```go
遍历map
for key, val := range m {
        fmt.Printf("%v===>%v\n", key, val)
	}

顺序遍历map
m := make(map[string]string)
    m["1"] = "hello"
    m["2"] = "world"
    m["3"] = "go"
    m["4"] = "is"
    m["5"] = "cool"

    sorted_keys := make([]string, 0)
    for k, _ := range m {
        sorted_keys = append(sorted_keys, k)
    }
    sort.Strings(sorted_keys)

    for _, k := range sorted_keys {
        fmt.Printf("%v=====>%v\n", k, m[k])
    }
```

# 线上数据库的分批查询

线上数据库的数据量非常大,一次性查出所有符合条件的row会很消耗内存
```
select * from datatable limit offset,amount
```
 这里就会有一个问题，随着offset值的越来越大，这条sql要扫描的表数据就会越来越多，因为要定位到offset这一行就需要扫描比offset小的所有行。显然在一张大数据量的表中，去这样做，性能就会出问题。为了避免这样的情形出现，我们自然会想到使用索引来解决，比如使用自增序列id进行分批取：
```
select * from datatable where id >= start and id<start+batchCount
```
这样我们对id建索引，然后分批去取，显然效果会高很多，但是如果自增序列由于删除等操作变得不是连续，就会出现空执行和多执行的情况出现
```
select * from datatable where id >= start limit batchCount
取出来之后,记得更新下次的id为返回的所有id中最大的,而不是start+batchCount
不过注意,这样起始id会被查询两次,有的业务会有问题
```
然后每次取回来我们再计算出起始id值，再去取下一批数据。这样就可以既避免了第一种不走索引，查询性能低下的问题，又解决了第二种id不连续，导致取回来的数据量不稳定导致浪费的问题了。

# Cron表达式(定时格式)

## * 星号

使用星号(*) 指示着你想在这个域上包含所有合法的值。例如，在月份域上使用星号意味着每个月都会触发这个 trigger。

0 * 17 * * ?

意义：每天从下午5点到下午5:59中的每分钟激发一次 trigger。它停在下午 5:59 是因为值 17 在小时域上，在下午 6 点时，小时变为 18 了，也就不再理会这个 trigger，直到下一天的下午5点。

## ? 问号

? 号只能用在日和周域上，但是不能在这两个域上同时使用。你可以认为 ? 字符是 "我并不关心在该域上是什么值。" 这不同于星号，星号是指示着该域上的每一个值。? 是说不为该域指定值。
不能同时这两个域上指定值的理由是难以解释甚至是难以理解的。基本上，假定同时指定值的话，意义就会变得含混不清了：考虑一下，如果一个表达式在日域上有值11，同时在周域上指定了 WED。那么是要 trigger 仅在每个月的11号，且正好又是星期三那天被激发？还是在每个星期三的11号被激发呢？要去除这种不明确性的办法就是不能同时在这两个域上指定值。

只要记住，假如你为这两域的其中一个指定了值，那就必须在另一个字值上放一个 ?。

表达式样例：

0 10,44 14 ? 3 WED

意义：在三月中的每个星期三的下午 2:10 和 下午 2:44 被触发。

## , 逗号

逗号 (,) 是用来在给某个域上指定一个值列表的。例如，使用值 0,15,30,45 在秒域上意味着每15秒触发一个 trigger。

表达式样例：

0 0,15,30,45 * * * ?

意义：每刻钟触发一次 trigger。

## 斜杠 (/)
是用于时间表的递增的。我们刚刚用了逗号来表示每15分钟的递增，但是我们也能写成这样 0/15。

表达式样例：

0/15 0/30 * * * ?

意义：在整点和半点时每15秒触发 trigger。

## - 中划线

中划线 (-) 用于指定一个范围。例如，在小时域上的 3-8 意味着 "3,4,5,6,7 和 8 点。"  域的值不允许回卷，所以像 50-10 这样的值是不允许的。

表达式样例：

0 45 3-8 ? * *

意义：在上午的3点至上午的8点的45分时触发 trigger。

## L 字母

L 说明了某域上允许的最后一个值。它仅被日和周域支持。当用在日域上，表示的是在月域上指定的月份的最后一天。例如，当月域上指定了 JAN 时，在日域上的 L 会促使 trigger 在1月31号被触发。假如月域上是 SEP，那么 L 会预示着在9月30号触发。换句话说，就是不管指定了哪个月，都是在相应月份的时最后一天触发 trigger。

表达式 0 0 8 L * ? 意义是在每个月最后一天的上午 8:00 触发 trigger。在月域上的 * 说明是 "每个月"。

当 L 字母用于周域上，指示着周的最后一天，就是星期六 (或者数字7)。所以如果你需要在每个月的最后一个星期六下午的 11:59 触发 trigger，你可以用这样的表达式 0 59 23 ? * L。

当使用于周域上，你可以用一个数字与 L 连起来表示月份的最后一个星期 X。例如，表达式 0 0 12 ? * 2L 说的是在每个月的最后一个星期一触发 trigger。

## W 字母

W 字符代表着平日 (Mon-Fri)，并且仅能用于日域中。它用来指定离指定日的最近的一个平日。大部分的商业处理都是基于工作周的，所以 W 字符可能是非常重要的。例如，日域中的 15W 意味着 "离该月15号的最近一个平日。" 假如15号是星期六，那么 trigger 会在14号(星期四)触发，因为距15号最近的是星期一，这个例子中也会是17号（译者Unmi注：不会在17号触发的，如果是15W，可能会是在14号(15号是星期六)或者15号(15号是星期天)触发，也就是只能出现在邻近的一天，如果15号当天为平日直接就会当日执行）。W 只能用在指定的日域为单天，不能是范围或列表值。

## 井号

字符仅能用于周域中。它用于指定月份中的第几周的哪一天。例如，如果你指定周域的值为 6#3，它意思是某月的第三个周五 (6=星期五，#3意味着月份中的第三周)。另一个例子 2#1 意思是某月的第一个星期一 (2=星期一，#1意味着月份中的第一周)。注意，假如你指定 #5，然而月份中没有第 5 周，那么该月不会触发。

示例：

表达式意义

"0 0 12 * *?" 每天中午12点触发

"0 15 10 ? **" 每天上午10:15触发

"0 15 10 * *?" 每天上午10:15触发

"0 15 10 * * ?*" 每天上午10:15触发

"0 15 10 * * ?2005" 2005年的每天上午10:15触发

"0 * 14 * *?" 在每天下午2点到下午2:59期间的每1分钟触发

"0 0/5 14 * *?" 在每天下午2点到下午2:55期间的每5分钟触发

"0 0/5 14,18 ** ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

"0 0-5 14 * *?" 在每天下午2点到下午2:05期间的每1分钟触发

"0 10,44 14 ? 3WED" 每年三月的星期三的下午2:10和2:44触发

"0 15 10 ? *MON-FRI" 周一至周五的上午10:15触发

"0 15 10 15 *?" 每月15日上午10:15触发

"0 15 10 L *?" 每月最后一日的上午10:15触发

"0 15 10 ? *6L" 每月的最后一个星期五上午10:15触发

"0 15 10 ? * 6L2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发

"0 15 10 ? *6#3" 每月的第三个星期五上午10:15触发

# go 中while和do while的实现

## while
```go
var i int = 1
    for {
        if i > 10 {
            break
        }
        fmt.Println("hello! ", i)
        i++
    }
```
## do while

```go
var i int = 1
    for {
        fmt.Println("hello! ", i)
        i++
     if i > 10 {
            break
        }
	}
```
# 指针
## 重要知识点
在Go语言中，直接砍掉了 C 语言指针最复杂的指针运算部分，只留下了获取指针（&运算符）和获取对象（*运算符）的运算，用法和C语言很类似。但不同的是，Go语言中没有->操作符来调用指针所属的成员，而与一般对象一样，都是使用.来调用。

Go 语言自带指针隐式解引用 ：对于一些复杂类型的指针， 如果要访问成员变量时候需要写成类似*p.field的形式时，只需要p.field即可访问相应的成员。

用new(structName)：这个方法得到的是*structName类型，即类的指针类型；

用structName{init para}：这个方法得到的是structName类型，即类的实例类型，不是指针

```go
package main

import (
    "fmt"
)

type Student struct {
    name   string
    age    int
    weight float32
    score  []int
}

func main(){
   pp := new(Student)                                  //使用 new 关键字创建一个指针
   *pp = Student{"qishuangming", 23, 65.0, []int{2, 3, 6}}
   fmt.Printf("stu pp have %d subjects\n", len((*pp).score))
   fmt.Printf("stu pp have %d subjects\n", len(pp.score)) //Go语言自带隐式解引用
}
```

## 引用与指针区别
指针是一个实体，而引用仅是个别名；

引用使用时无需解引用(*)，指针需要解引用；

引用只能在定义时被初始化一次，之后不可变；指针可变；

引用没有 const，指针有 const；const修饰的指针不可变；

引用不能为空，指针可以为空；

**从内存分配上看：程序为指针变量分配内存区域，而引用不需要分配内存区域。**

“sizeof 引用”得到的是所指向的变量(对象)的大小，而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小；
指针和引用的自增(++)运算意义不一样；

**两者都是地址的概念，指针指向一块儿内存，其内容为所指内存的地址；引用是某块儿内存的别名。**

# 值类型与引用类型的区别

**数组和结构体是值类型,map slice是引用类型**
'

## 声明变量时候的却别
```go
package main
import (
 "fmt"
)
func main() {
 var i *int
 *i=10
 fmt.Println(*i)
}
这个例子会打印出什么？0还是10?。以上全错，运行的时候会painc，原因如下：

panic: runtime error: invalid memory address or nil pointer dereference

```

对于引用类型的变量，我们不光要声明它，还要为它分配内容空间，否则我们的值放在哪里去呢？这就是上面错误提示的原因

对于值类型的声明不需要，是因为已经默认帮我们分配好了
分配内存，Go提供了两种方式，分别是new和make

## 数值传递上的区别
值类型在赋值给另一个变量的时候是值拷贝,引用类型则是指针拷贝(地址拷贝)

只要区别在于函数传参的时候,因为数组和结构体是值类型,形参的改变不会导致传参的改变. 如果传地址则可以改变传参

```go
type person struct {
	name string
	age int
}

func main() {
	p:=person{
		name:"sx"
		age:12
	}

	change (&p)
	uableChange(p)
}
func change (p *person){
	p.age--
}
func unableChange(p person)[
	p.age++
]
```
##  在new 和 make的区别
### new
Go提供内建函数new

func new(Type) *Type

它只接受一个参数，这个参数是一个类型，分配好内存后，返回一个指向该类型内存地址的指针。同时请注意它同时把分配的内存置为零，也就是类型的零值。那么上面的函数可以改写成
```go
func main() {
 var i *int
 i=new(int)
 *i=10
 fmt.Println(*i)
}
```
这就是new，它返回的永远是类型的指针，指向分配类型的内存地址
### make
make也是用于内存分配的，但和new不同，它只用于通道chan、映射map以及切片slice的内存创建

它的返回的类型就是这三个类型本身，而不是他们的指针类型，因为这三种类型本身就是引用类型

注意，因为这三种类型是引用类型，所以必须得初始化，但不是置为零值

func make(t Type, size ...IntegerType) Type
### new 和 make的区别
二者都是内存的分配（堆上），但是make只用于slice、map以及channel的初始化（非零值）；而new用于类型的内存分配，并且内存置为零

make返回的还是这三个引用类型本身；而new返回的是指向类型的指针
new不常用，通常都是采用短语句声明以及结构体的字面量达到我们的目的，比如：
```go
i:=0
u:=user{}
```
make函数是无可替代的

# slice
复制的时候,数组是值拷贝,slice是指针拷贝
## 介绍
```
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
```
ptr指的是指向array的pointer，len是指切片的长度, cap指的是切片的容量。

## 扩容

slice这种数据结构便于使用和管理数据集合，可以理解为是一种“动态数组”，扩容规则是:

+ 如果切片的容量小于1024个元素，那么扩容的时候slice的cap就翻番，乘以2；

+ 一旦元素个数超过1024个元素，增长因子就变成1.25，即每次增加原来容量的四分之一。

+ 如果扩容之后，还没有触及原数组的容量，那么，切片中的指针指向的位置，就还是原数组，如果扩容之后，超过了原数组的容量，那么，Go就会开辟一块新的内存，把原来的值拷贝过来，这种情况丝毫不会影响到原数组。

```go
举例:
import (
    "fmt"
)
func main(){
    arr := [4]int{10, 20, 30, 40}
    slice := arr[0:2]
    testSlice1 := slice
    testSlice2 := append(append(append(slice, 1),2),3)
    slice[0] = 11

    fmt.Println(testSlice1[0])
    fmt.Println(testSlice2[0])
}
输出：

11
10
```
## nil切片和空切片

空切片一般会用来表示一个空的集合。比如数据库查询，一条结果也没有查到，那么就可以返回一个空切片。

空切片和 nil 切片的区别在于，空切片指向的地址不是nil，指向的是一个内存地址，但是它没有分配任何内存空间，即底层元素包含0个元素。

```go
nil切片
var snil []int
空切片
var sempty :=make([]int,0)  或
var sempty :=[]int{}

```
## 切片的三种定义方式,map两种

### 1.定义一个切片，然后让切片去引用一个已经创建好的数组

```go
package main
import (
    "fmt"
)

func main() {
    var arr [5]int = [...]int {1, 2, 3, 4, 5}
    var slice = arr[1:3]
    fmt.Println("arr=", arr)
    fmt.Println("slice=", slice)
    fmt.Println("slice len", len(slice))
    fmt.Println("slice cap", cap(slice))
}
```

### 2.通过make来创建切片。

基本语法：var 切片名 []type = make([], len, [cap])；参数说明：type是数据类型、len是大小、cap是切片容量（容量必须>=长度）

通过make方式创建切片可以指定切片大小和容量
如果没有给切片的各个元素赋值，那么就会使用默认值(int、float=>0, strint=>"", bool=>false)
荣国make方式创建的切片对应的数组是由make底层维护，对外不可见，也就是只能通过slice访问各个元素

```go

package main
import (
    "fmt"
)

func main() {
    var slice []float64 = make([]float64, 5, 10)
    //没有给值，默认都是0
    fmt.Println(slice)  //[0 0 0 0 0]

    //赋值
    slice[1] = 5
    slice[3] = 10
    fmt.Println(slice)  //[0 5 0 10 0]

    fmt.Println("slice大小:", len(slice)) //slice大小: 5
    fmt.Println("slice容量:", cap(slice)) //slice容量: 10
}
```
### 3.定义一个切片，直接就指定具体数组，使用原理类似于make的方式

```go
package main
import (
    "fmt"
)

func main() {
    var slice []string = []string{"zhangsan", "lisi", "wangwu"}
    fmt.Println("slice=", slice) //slice= [zhangsan lisi wangwu]
    fmt.Println("slice len", len(slice)) //slice len 3
    fmt.Println("slice cap", cap(slice)) //slice cap 3
}
```

### 第一种和第二种的区别

第一种方式是直接引用数组，这个数组是事先存在的，程序员可见
第二种方式是通过make来创建切片，make也会创建一个数组，是由切片在底层维护，程序员不可见

### 共享数组产生bug

多个切片引用一个数组时候,扩容以后并没有新建一个新的数组，扩容前后的数组都是同一个，这也就导致了新的切片修改了一个值，也影响到了老的切片了。并且 append() 操作也改变了原来数组里面的值。一个 append() 操作影响了这么多地方，如果原数组上有多个切片，那么这些切片都会被影响！无意间就产生了莫名的 bug！

# map
## 创建

## 底层原理

https://i6448038.github.io/2018/08/26/map-secret/

一个map是一个bucket数组,bucket可以链式追加,如图:

根据key经过hash计算处的值会分为高八位和低八位,低八位用于寻找位于map的哪个bucket,高八位用于寻找bucket内部的哪个key

go的bucket内部并不是key0/value0/key1/value1的形式，这样做的好处是：在key和value的长度不同的时候，可以消除padding带来的空间浪费。

## map的扩容
当以上的哈希表增长的时候，Go语言会将bucket数组的数量扩充一倍，产生一个新的bucket数组，并将旧数组的数据迁移至新数组。

## 加载因子
判断扩充的条件，就是哈希表中的加载因子(即loadFactor)。

加载因子是一个阈值，一般表示为：散列包含的元素数 除以 位置总数。是一种“产生冲突机会”和“空间使用”的平衡与折中：加载因子越小，说明空间空置率高，空间使用率小，但是加载因子越大，说明空间利用率上去了，但是“产生冲突机会”高了。

每种哈希表的都会有一个加载因子，数值超过加载因子就会为哈希表扩容。

当Go的map长度增长到大于加载因子所需的map长度时，Go语言就会将产生一个新的bucket数组，然后把旧的bucket数组移到一个属性字段oldbucket中。**注意：并不是立刻把旧的数组中的元素转义到新的bucket当中，而是，只有当访问到具体的某个bucket的时候，会把bucket中的数据转移到新的bucket中。并且不会直接删除旧的bucket，而是把原来的引用去掉，利用GC清除内存。**

# channel

关闭已经关闭的管道会有panic

关闭有缓冲的chan时,会立即禁止写入,但可以读取,读取完之后才返回false

## 管道关闭原则

**没有任何情况会迫使你违反通道关闭原则。 如果你遇到这样的一个情况，请你重新想想你的设计并重写你的代码。**

不允许**接收方**关闭通道

发送者不能关闭一个有多个并发发送者的通道

## select

在UNIX中，select()函数用来监控一组描述符，该机制常被用于实现高并发的socket服务器程序。Go语言直接在语言级别支持select关键字，用于处理异步IO问题，大致结构如下：

```go
select {
    case <- chan1:
    // 如果chan1成功读到数据

    case chan2 <- 1:
    // 如果成功向chan2写入数据

    default:
    // 默认分支
}
```

# 协程

## 线程和进程的关系

## 线程和协程的关系区别

### 内存结构

进程是数据,文件和代码的集合.

线程等于堆栈资源和程序计数器(装字节码执行进度)

协程是多个协程存储在一个线程中,协程的堆资源在线程中,栈资源也用线程的堆来保存.所以多个协程可以在一个线程里面运行.

### MPG模式

多个协程G对应多份上下文P,都由一个M来维护.当某个协程G0需要阻塞的时候.就把P0拿下来,换P1上去,继续用M来执行

### 切换模式开销

线程切换时,因为线程的调度是在内核态运行的，而线程中的代码是在用户态运行,会导致切换开销.协程切换在线程内部完成,不会导致模式切换.

### io的优化

golang 对各种 io函数 进行了封装，内部调用操作系统的异步 io函数，当这些异步函数返回 busy 或 bloking 时，golang 利用这个时机将现有的执行序列压栈，让线程去拉另外一个协程的代码来执行，利用并封装了操作系统的异步函数。包括 linux 的 epoll、select.

epoll的优势,文件描述符数量限制更大,零拷贝技术(内核态和用户态有一块共享内存).

## 协程(goroutine)

go的主线程(进程级别)内,可以开启多个协程(编译器级别优化,轻量级的线程)

协程特点:
有独立的栈空间
共享程序堆内存
调度由用户控制
协程是轻量级的线程

协程的资源消耗要小于线程,这是go语言的在并发上的巨大优势.线程等于堆资源+栈 ,协程是多个协程存储在一个线程中,协程的堆资源在线程中,栈资源用线程的堆来保存.所以多个协程可以在一个线程里面运行.

## MPG模式

m是主线程,p是上下文,g是协程

##

## 原理
协程是基于线程的。内部实现上，维护了一组数据结构和 n 个线程，真正的执行还是线程，协程执行的代码被扔进一个待执行队列中，由这 n 个线程从队列中拉出来执行。这就解决了协程的执行问题。那么协程是怎么切换的呢？

答案是：golang 对各种 io函数 进行了封装，这些封装的函数提供给应用程序使用，而其内部调用了操作系统的异步 io函数，当这些异步函数返回 busy 或 bloking 时，golang 利用这个时机将现有的执行序列压栈，让线程去拉另外一个协程的代码来执行，基本原理就是这样，利用并封装了操作系统的异步函数。包括 linux 的 epoll、select 和 windows 的 iocp、event 等。
## 改变线程多路复用的尴尬

在一些高并发的网络服务器编程中，使用一个线程服务一个 socket 连接是很不明智的。于是操作系统提供了基于事件模式的异步编程模型。用少量的线程来服务大量的网络连接和I/O操作。但是采用异步和基于事件的编程模型，复杂化了程序代码的编写，非常容易出错。因为线程穿插，也提高排查错误的难度。

 　　协程，一个高并发的网络服务器，每一个socket连接进来，服务器用一个协程来对他进行服务。代码非常清晰。而且兼顾了性能。

## 特点
### 内存占用少
每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。
goroutine：2KB
线程：8MB

### 非抢占(非公平)
对于 协程(用户级线程)，这是对内核透明的，也就是系统并不知道有协程的存在，是完全由用户自己的程序进行调度的，因为是由用户程序自己控制，那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程，通常只能进行 协作式调度，**需要协程自己主动把控制权转让出去**之后，其他协程才能被执行到。

### 切换开销小
goroutine 切换开销方面远比线程小

线程：涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新等。

goroutine：只有三个寄存器的值修改 - PC / SP / DX.

## 主从函数并发
```go
func printHello() {
	fmt.Println("hello world goroutine")
}

func main() {
	go printHello()    // 创建了协程
	fmt.Println("main goroutine")
}
```
```
输出
main goroutine
```

当协程创建完毕之后，主函数立即返回继续执行下一行代码，不像函数调用，需要等函数执行完成。主协程执行完毕，程序便退出，printHello 协程随即也退出，便不会有输出。

# strings.NewReader()

NewReader创建一个从s读取数据的Reader
```
type Reader struct {
	s        string //对应的字符串
	i        int64  // 当前读取到的位置
	prevRune int
}
```

## Len,Size，Read方法

Len作用: 返回未读的字符串长度

Size的作用:返回字符串的长度

read的作用: 读取字符串信息
```go
	r := strings.NewReader("abcdefghijklmn")
	fmt.Println(r.Len())   // 输出14  初始时，未读长度等于字符串长度
	var buf []byte
	buf = make([]byte, 5)
	readLen, err := r.Read(buf)
	fmt.Println("读取到的长度:", readLen) //读取到的长度5
	if err != nil {
		fmt.Println("错误:", err)
	}
	fmt.Println(buf)            //adcde
	fmt.Println(r.Len())        //9   读取到了5个 剩余未读是14-5
	fmt.Println(r.Size())       //14   字符串的长度
```
# wait group
WaitGroup 对象内部有一个计数器，最初从0开始，它有三个方法：Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ，Done() 每次把计数器-1 ，wait() 会阻塞代码的运行，直到计数器地值减为0。

```go
func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}
```
## WaitGroup对象不是一个引用类型
WaitGroup对象是一个结构体,在go语言中结构体不是一个引用类型，在通过函数传值的时候需要使用地址：
```go
unc main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go f(i, &wg)
    }
    wg.Wait()
}

// 一定要通过指针传值，不然进程会进入死锁状态
func f(i int, wg *sync.WaitGroup) {
    fmt.Println(i)
    wg.Done()
}
```
# 闭包

# sql timestamp

在go里面用string来接收对应的字段

# Go Module

## go mod init
在项目根目录下执行  go mod init 项目名

这时可看到目录内多了 go.mod 文件，内容很简单只有两行：

module github.com/islishude/gomodtest

go 1.12
首行为当前的模块名称，接下来是 go 的使用版本。这两行和 npm package.json 的 name 和 engine 字段的功能很类似。

然后新建一个 main.go 写入以下内容，这里我们引用了 rsc.io/quote 包，注意我们现在还没有下载这个包。
```go
package main

import (
	"fmt"

	"rsc.io/quote"
)

func main() {
	fmt.Println(quote.Hello())
}
```
如果是默认情况下，使用 go run main.go 肯定会提示找不到这个包的错误，但是当前 gomod 模式，如果没有此依赖回先下载这个依赖。

$ go run main.go
go: finding rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
Hello, world.
因为包含 http://golang.org 下的包，记得设置代理。这个时候当前包目录除了 go.mod 还有一个 go.sum 的文件，这个类似 npm package-lock.json。

gomod 不会在 $GOPATH/src 目录下保存 rsc.io 包的源码，而是包源码和链接库保存在 $GOPATH/pkg/mod 目录下。

$ ls $GOPATH/pkg/mod
cache      golang.org rsc.io

除了 go run 命令以外，go build、go test 等命令也能自动下载相关依赖包。

## 安装依赖
如果要想先下载依赖，那么可以直接像以前那样 go get 即可，不过 gomod 下可以跟语义化版本号，比如 go get foo@v1.2.3，也可以跟 git 的分支或 tag，比如go get foo@master，当然也可以跟 git 提交哈希，比如 go get foo@e3702bed2。需要特别注意的是，gomod 除了遵循语义化版本原则外，还遵循最小版本选择原则，也就是说如果当前版本是 v1.1.0，只会下载不超过这个最大版本号。如果使用 go get foo@master，下次在下载只会和第一次的一样，无论 master 分支是否更新了代码，如下所示，使用包含当前最新提交哈希的虚拟版本号替代直接的 master 版本号。

$ go get golang.org/x/crypto/sha3@master
go: finding golang.org/x/crypto/sha3 latest
go: finding golang.org/x/crypto latest
$ cat go.mod
module github.com/adesight/test

go 1.12

require (
	golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
	rsc.io/quote v1.5.2
)
如果下载所有依赖可以使用 go mod download 命令。

## 移除依赖
当前代码中不需要了某些包，删除相关代码片段后并没有在 go.mod 文件中自动移出。

运行下面命令可以移出所有代码中不需要的包：

go mod tidy

如果仅仅修改 go.mod 配置文件的内容，那么可以运行 go mod edit --droprequire=path，比如要移出 golang.org/x/crypto 包

go mod edit --droprequire=golang.org/x/crypto

## 查看依赖包

go list -m all

# go build,install,get区别

[关于编译链接,词法语法分析的好文](https://www.cnblogs.com/qcrao-2018/p/11124360.html)

## go build
在当前目录下编译生成可执行文件。依赖包编译的.a文件存放在临时文件夹,编译结束后删除

## go install
如果为可执行文件（package "main"且包含main方法），则会编译生成可执行文件到【$GOPATH/bin】目录下

如果为可执行文件import引入的其他包 ，就会被编译到【GOPATH/pkg】目录下。

go install 也可以完成类似【go build】的功能，区别在于【go install】会生成二进制文件到【GOPATH/pkg】目录下,会把可执行文件自动生成到【GOBIN】目录下，这是在一开始被配置到【$PATH】中了，这样就可以使项目可执行文件能被方便的全局调用。

# context  源码阅读

## 简介

golang 的 Context包，是专门用来简化对于处理单个请求的多个goroutine之间与请求域的数据、取消信号、截止时间等相关操作，这些操作可能涉及多个 API 调用。
比如有一个网络请求Request，每个Request都需要开启一个goroutine做一些事情，这些goroutine又可能会开启其他的goroutine。这样的话， 我们就可以通过Context，来跟踪这些goroutine，并且通过Context来控制他们的目的，这就是Go语言为我们提供的Context，中文可以称之为“上下文”。

另外一个实际例子是，在Go服务器程序中，每个请求都会有一个goroutine去处理。然而，处理程序往往还需要创建额外的goroutine去访问后端资源，比如数据库、RPC服务等。由于这些goroutine都是在处理同一个请求，所以它们往往需要访问一些共享的资源，比如用户身份信息、认证token、请求截止时间等。而且如果请求超时或者被取消后，所有的goroutine都应该马上退出并且释放相关的资源。这种情况也需要用Context来为我们取消掉所有goroutine

## 源码

context接口的核心:
```go
type Context interface {

Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}

}
```
Deadline方法是获取设置的截止时间的意思，第一个返回式是截止时间，到了这个时间点，Context会自动发起取消请求；第二个返回值ok==false时表示没有设置截止时间，如果需要取消的话，需要调用取消函数进行取消。

Done方法返回一个只读的chan，类型为struct{}，我们在goroutine中，如果该方法返回的chan可以读取(c.done 被close的时候可以被读取)，则意味着parent context已经发起了取消请求，我们通过Done方法收到这个信号后，就应该做清理操作，然后退出goroutine，释放资源。之后，Err 方法会返回一个错误，告知为什么 Context 被取消。

Err方法返回取消的错误原因，因为什么Context被取消。

Value方法获取该Context上绑定的值，是一个键值对，所以要通过一个Key才可以获取对应的值，这个值一般是线程安全的。

## context的根源上下文

```go
var (
	background = new(emptyCtx)

	todo = new(emptyCtx)
)

func Background() Context {

	return background

}

func TODO() Context {

	return todo
}

```

一个是Background，主要用于main函数、初始化以及测试代码中，作为Context这个树结构的最顶层的Context，也就是根Context，它不能被取消。

一个是TODO，如果我们不知道该使用什么Context的时候，可以使用这个，但是实际应用中，暂时还没有使用过这个TODO。

他们两个本质上都是emptyCtx结构体类型，是一个不可取消，没有设置截止时间，没有携带任何值的Context。

```go
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {

	return
}

func (*emptyCtx) Done() <-chan struct{} {

	return nil
}

func (*emptyCtx) Err() error {

	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {

	return nil
}
```

## 其他类型的context
WithCancel函数，传递一个父Context作为参数，返回子Context，以及一个取消函数用来取消Context。

WithDeadline函数，和WithCancel差不多，它会多传递一个截止时间参数，意味着到了这个时间点，会自动取消Context，当然我们也可以不等到这个时候，可以提前通过取消函数进行取消。

WithTimeout和WithDeadline基本上一样，这个表示是超时自动取消，是多少时间后自动取消Context的意思。

WithValue函数和取消Context无关，它是为了生成一个绑定了一个键值对数据的Context，这个绑定的数据可以通过Context.Value方法访问到，这是我们实际用经常要用到的技巧，一般我们想要通过上下文来传递数据时，可以通过这个方法，如我们需要tarce追踪系统调用栈的时候。

## WithCancel源码阅读
context.WithCancel生成了一个withCancel的实例以及一个cancelFuc，这个函数就是用来关闭ctxWithCancel中的 Done channel 函数。
下面来分析下源码实现，首先看看初始化，如下：
```go
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{
		Context: parent,
		done:    make(chan struct{}),
	}
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}
newCancelCtx返回一个初始化的cancelCtx，cancelCtx结构体继承了Context，实现了canceler方法：
//*cancelCtx 和 *timerCtx 都实现了canceler接口，实现该接口的类型都可以被直接canceled
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

type cancelCtx struct {
    Context
    done chan struct{} // closed by the first cancel call.
    mu       sync.Mutex
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error             // 当其被cancel时将会把err设置为非nil
}

func (c *cancelCtx) Done() <-chan struct{} {
    return c.done
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
}

//核心是关闭c自己的done通道
//同时会设置c.err = err, c.children = nil
//依次遍历c.children，每个child分别cancel
//如果设置了removeFromParent，则将c从其parent的children中删除
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    close(c.done)
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c) // 从此处可以看到 cancelCtx的Context项是一个类似于parent的概念
    }
}
```
可以看到，所有的children都存在一个map中；Done方法会返回其中的done channel， 而另外的cancel方法会关闭Done channel并且逐层向下遍历，关闭children的channel，并且将当前canceler从parent中移除。

WithCancel初始化一个cancelCtx的同时，还执行了propagateCancel方法，最后返回一个cancel function。

propagateCancel 方法定义如下：
```go
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
```
propagateCancel 的含义就是传递cancel，从当前传入的parent开始（包括该parent），向上查找最近的一个可以被cancel的parent， 如果找到的parent已经被cancel，则将方才传入的child树给cancel掉，否则，将child节点直接连接为找到的parent的children中（Context字段不变，即向上的父亲指针不变，但是向下的孩子指针变直接了）； 如果没有找到最近的可以被cancel的parent，即其上都不可被cancel，则启动一个goroutine等待传入的parent终止，则cancel传入的child树，或者等待传入的child终结。

## golang的redis包

[Golang操作redis指南](https://www.jianshu.com/p/89ca34b84101)

### redigo模块的详细使用

#### 连接

`Conn`接口是redis操作过程中比较重要的接口。应用一般通过调用`Dial、DialWithTimeout、NewConn`函数来创建一个链接。

 `注意：`应用必须在操作redis结束之后去调用该连接的`Close`方法来关闭连接，以防止资源消耗以及其他问题。

**Conn接口下的相关方法**

```go
type Conn interface {
    // 关闭链接
    Close() error

    // 当链接不可用时返回非空值
    Err() error

    //Do方法向redis服务端发送命令并返回接收到响应
    Do(commandName string, args ...interface{}) (reply interface{}, err error)

    //Send 将相关的命令写入客户端的buffer中
    Send(commandName string, args ...interface{}) error

    // Flush 将客户端的输出缓冲内容刷新到redis服务器端
    Flush() error

    // Receive 从redis服务端接收单个回复
    Receive() (reply interface{}, err error)
}
```

#### 使用Pipelining操作redis

`Conn`会使用`Send、Flush、Receive`来支持pipeline操作。

```tsx
Send(commandName string, args ...interface{}) error
Flush() error
Receive() (reply interface{}, err error)
```

`Send`会写入命令到连接的输出缓冲区里。`Flush`会将输出缓冲区中的数据刷新到服务端。`Receive`回去荣服务端读取单个响应。

```swift
c.Send("SET", "foo", "bar")
c.Send("GET", "foo")
c.Flush()
c.Receive()
v, err = c.Receive()
```

其实，在`Do`方法中会包含`Send/Flush/Receive`等方法。

使用`Send`和`Do`方法来实现pipeline。

```swift
c.Send("MULTI")
c.Send("INCR", "foo")
c.Send("INCR", "bar")
r, err := c.Do("EXEC")
fmt.Println(r) //[1,1
```

`注意:`建议使用redis自身包含的命令进行批量操作而不是使用pipelining，比如`mset、mget、hmset、hmget`等等。原子性可能会更好一些

1

<file_sep>/notes/Java-设计模式.md

- [动态代理](#动态代理)
    - [定义](#定义)
    - [继承与聚合来扩展某个方法的功能](#继承与聚合来扩展某个方法的功能)
        - [继承](#继承)
        - [聚合](#聚合)
    - [静态代理](#静态代理)
    - [动态代理原理解析](#动态代理原理解析)
        - [三个角色的好处](#三个角色的好处)
    - [简单归纳：](#简单归纳)
    - [动态代理的优点](#动态代理的优点)
- [简单工厂,普通工厂,抽象工厂,](#简单工厂普通工厂抽象工厂)

# 动态代理

[参考博客](https://juejin.im/post/5a99048a6fb9a028d5668e62#heading-4)

## 定义

实现InvocationHandler的invoke方法,就可以用handler来代替传入的对象来实现一些方法.而且传入的对象是任意的,要实现的方法也是任意的.

比如,在invoke方法中,实现bird对象的fly()方法,让fly()方法可以打印日志.

## 继承与聚合来扩展某个方法的功能

### 继承

创建一个bird类实现fly方法:

```java
public interface Flyable {
    void fly();
}

public class Bird implements Flyable {

    @Override
    public void fly() {
        System.out.println("Bird is flying...");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
```

如果我现在想让fly()不仅要飞,还要计算飞的时间,一种不太理想的做法是创建一个Bird2类继承Bird,重写Bird的fly()方法,在里面实现记录时间:

```java
public class Bird2 extends Bird {

    @Override
    public void fly() {
        long start = System.currentTimeMillis();

        super.fly();

        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }
}

```

弊端:每次对fly()有新的需求,就要新创建一个继承自Bird的子类

### 聚合

还有一种解决方案叫做：聚合。 我们再次创建新类Bird3，在Bird3的构造方法中传入Bird实例。同时，让Bird3也实现Flyable接口，并在fly方法中调用传入的Bird实例的fly方法：

```java
public class Bird3 implements Flyable {
    private Bird bird;

    public Bird3(Bird bird) {
        this.bird = bird;
    }

    @Override
    public void fly() {
        long start = System.currentTimeMillis();

        bird.fly();

        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }
}
```

使用继承将导致在对fly()有新的需求时,类无限制扩展，同时灵活性也无法获得保障。那么，使用 聚合 是否可以避免这个问题,但我们的类需要稍微改造一下。修改Bird3类，将聚合对象Bird类型修改为Flyable

```java
//把Bird3更名为BirdLogProxy,改聚合对象Bird为Flyable,然后可以修改内部的
//fly()方法
public class BirdLogProxy implements Flyable {
    private Flyable flyable;

    public BirdLogProxy(Flyable flyable) {
        this.flyable = flyable;
    }

    @Override
    public void fly() {
        System.out.println("Bird fly start...");

        flyable.fly();

        System.out.println("Bird fly end...");
    }
}

```

为什么聚合可以灵活的修改某个方法的功能而不用添加新的子类

## 静态代理

接下来，观察上面的类BirdTimeProxy，在它的fly方法中我们直接调用了flyable->fly()方法。换而言之，BirdTimeProxy其实代理了传入的Flyable对象，这就是典型的静态代理实现。
从表面上看，静态代理已经完美解决了我们的问题。可是，试想一下，如果我们需要计算SDK中100个方法的运行时间，同样的代码至少需要重复100次，并且创建至少100个代理类。往小了说，如果Bird类有多个方法，我们需要知道其他方法的运行时间，同样的代码也至少需要重复多次。因此，静态代理至少有以下两个局限性问题：

如果同时代理多个类，依然会导致类无限制扩展
如果类中有多个方法，同样的逻辑需要反复实现

那么，我们是否可以使用同一个代理类来代理任意对象呢？我们以获取方法运行时间为例，是否可以使用同一个类（例如：TimeProxy）来计算任意对象的任一方法的执行时间呢？甚至再大胆一点，代理的逻辑也可以自己指定。比如，获取方法的执行时间，打印日志，这类逻辑都可以自己指定。这就是本文重点探讨的问题，也是最难理解的部分：动态代理。

## 动态代理原理解析

我们用**代理实例生成代码**+自定义实现InvocationHandler接口的类来生成**代理代码**,在使用的时候把代理代码编译加载进内存即可实现动态代理

图中就是**代理实例生成代码**,它的作用是用反射来生成**代理代码**
传入的参数是Class inf,即我们要自己实现的InvocationHandler接口

我们要实现的InvocationHandler接口的invoke方法:

```java
public class MyInvocationHandler implements InvocationHandler {
    private Bird bird;

    public MyInvocationHandler(Bird bird) {
        this.bird = bird;
    }

    @Override
    public void invoke(Object proxy, Method method, Object[] args) {
        long start = System.currentTimeMillis();

        try {
            method.invoke(bird, new Object[] {});
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }
}

```

最终生成的动态代理代码:

```java
package com.youngfeng.proxy;

import java.lang.Override;
import java.lang.reflect.Method;

public class TimeProxy implements Flyable {
  private InvocationHandler handler;

  public TimeProxy(InvocationHandler handler) {
    this.handler = handler;
  }

  @Override
  public void fly() {
    try {
    	Method method = com.youngfeng.proxy.Flyable.class.getMethod("fly");
    	this.handler.invoke(this, method, null);
    } catch(Exception e) {
    	e.printStackTrace();
    }
  }
}

最后的方法栈的调用关系如图:

```

在静态代理部分，我们在代理类中传入了被代理对象。可是，使用newProxyInstance生成动态代理对象的时候，我们居然不再需要传入被代理对象了。我们传入了的实际对象是InvocationHandler实现类的实例，这看起来有点像生成了InvocationHandler的代理对象，在动态生成的代理类的任意方法中都会间接调用InvocationHandler->invoke(proxy, method, args)方法。

其实的确是这样。TimeProxy真正代理的对象就是InvocationHandler，不过这里设计的巧妙之处在于，InvocationHandler是一个接口，真正的实现由用户指定。另外，在每一个方法执行的时候，invoke方法都会被调用 ，这个时候如果你需要对某个方法进行自定义逻辑处理，可以根据method的特征信息进行判断分别处理。

### 三个角色的好处

这样设计有什么好处呢？

如果我们还需要对其它任意对象进行代理，是否还需要改动newProxyInstance方法的源码，答案是：完全不需要！

只要你在newProxyInstance方法中指定代理需要实现的接口，指定用于自定义处理的InvocationHandler对象，整个代理的逻辑处理都在你自定义的InvocationHandler实现类中进行处理。至此，而我们终于可以从不断地写代理类用于实现自定义逻辑的重复工作中解放出来了，从此需要做什么，交给InvocationHandler。

事实上，我们之前给自己定下的目标“使用同一个类来计算任意对象的任一方法的执行时间”已经实现了。严格来说，是我们超额完成了任务，TimeProxy不仅可以计算方法执行的时间，也可以打印方法执行日志，这完全取决于你的InvocationHandler接口实现。因此，这里取名为TimeProxy其实已经不合适了。我们可以修改为和JDK命名一致，即$Proxy0.

## 简单归纳：

Proxy->newProxyInstance(infs, handler) 用于生成代理对象
InvocationHandler：这个接口主要用于自定义代理逻辑处理
为了完成对被代理对象的方法拦截，我们需要在InvocationHandler对象中传入被代理对象实例

## 动态代理的优点

动态代理我们居然可以在不改变源码的情况下，直接在方法中插入自定义逻辑。这有点不太符合我们的一条线走到底的编程逻辑，这种编程模型有一个专业名称叫 AOP。所谓的AOP，就像刀一样，抓住时机，趁机插入。

基于这样一种动态特性，我们可以用它做很多事情，例如：

事务提交或回退（Web开发中很常见）
权限管理
自定义缓存逻辑处理
SDK Bug修复 ...

# 简单工厂,普通工厂,抽象工厂,

https://www.zhihu.com/question/20367734

# JDK中的设计模式

## 工厂模式

创建对象而不将实例化逻辑暴露给客户端，并通过公共接口引用新创建的对象。

例如：

- java.lang.Object#toString() (在所有子类中重写)
- java.lang.Class#newInstance()
- java.lang.Integer#valueOf(String)
- java.lang.Class#forName()
- java.lang.reflect.Array#newInstance()
- java.lang.reflect.Constructor#newInstance()

## 抽象工厂

javax.xml.parsers.DocumentBuilderFactory抽象类
public static DocumentBuilderFactory newInstance()方法
类功能：使得应用程序可以通过XML文件，获得一个能生成DOM对象的解析器。
方法功能：获取一个DocumentBuilderFactory的新实例。这一静态方法会创建一个新的工厂实例。

## 单例模式

java.lang.RunTime类
public static Runtime getRuntime()
类功能：每一个运行的java应用都会有一个唯一的RunTime类的实例，这个实例使得应用程序在运行期间能够受到运行环境的影响。
方法功能：返回一个和当前java应用关联的RunTime对象。
实现方式：

```java
private static Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
    return currentRuntime;
}
```

## 适配器模式

java.util.Arrays
public static List asList(T… a)方法
类功能：此类包含了大量对数组操作的方法。
方法功能：将一个引用类型的数组转为一个List。从而可以使用List类的操作来操作数组对象，但是有一点要注意：就是不能使用add(),remove()操作，因为返回的list底层是基于数组的，数组结构是不能更改的。 list类就是这里的适配器，通过这个适配器，对数组的直接操作变为间接操作。

## 装饰器模式

- Reader抽象类和Writer抽象类有相同的构造器函数。
- 构造器函数
- 类功能：Reader抽象类用于读一个字符集流；Writer抽象类用于写一个字符集流。
- 方法功能：就是构造函数的功能

## 享元模式

java.lang.Integer(其它基本类型包装类(除去Float,Double)也如此,还有BigDecimal)
Integer.valueOf()方法
byte，short，int，long，boolean，char的包装型在类加载到JVM时，已经缓存了制定范围的对象引用，因为值的设定使用的是static块或者常量。其中char的范围为：0～127；boolean的值为true和false；其它默认范围都是－127～128。其中int的上限127可以调整，这需要调整JVM的参数。
同时利用了享元模式的还有String这个类，因为生存的每个字符串都是不可变的。

## 代理模式

java.lang.reflect.Proxy类
原理：代理提供了一个static方法用于创建一个动态代理类和被代理类的实例。它是所有通过此方式创建动态代理类的父类。
使用方式：

```java
InvocationHandler handler = new MyInvocationHandler(...);
class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);
Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);
```

也可以更简单的写为:

```java
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[]Foo.class},handler);
```

## 职责链模式

java.util.logging.Logger类
public void log(LogRecord record)
类功能：为系统or组件记录日志消息。如何体现了职责链模式：每个记录器都跟踪“父”记录器，所谓”父”记录器，就是Logger命名空间中最近的现有祖先。
方法功能：用于记录日志信息。这一类中所有其它的日志方法都是通过调用这一方法实现日志记录的。子类能够覆写这一方法从而获取所有的日志行为。

## 命令模式

java.lang.Runnable
所有对Runable接口的实现类
类功能：实现Runable接口的类，可以被线程执行。
如果体现了命令模式：通过实现Runable接口的类，将请求封装为一个对象，对请求排队或记录请求日志，以及支持可撤销操作。允许接受请求的一方决定是否要否决请求，最重要一点就是：命令模式把请求一个操作的对象和怎么执行一个操作的对象解耦。这就是Excutor框架执行实现Runable接口任务类的体现。

## 解释器模式

java.text.Format
类功能：此抽象类用于格式化一些格式敏感的信息，如：日期，信息，数字等。
实现方式：此类定义了一些方法，用于：将格式敏感的信息转为String。

## 迭代器模式

这个就不说了，很多集合已经使用了迭代器进行遍历

## 备忘录模式

java.io.Serializable接口
接口功能：只有实现了Serializable接口的类才能序列化，此接口中没有任何方法，只是为类标记实现了此接口的类可以进行序列化。而如果一个类想要序列化，除了实现这个接口外，还要自己写ReadObject()，WriteObject()方法，用于对流的读取和输出。

## 观察者模式

类观察到变量的变化,让类们自动改变

# Spring中的设计模式

## 工厂设计模式

定义一个创建对象的接口，让其子类自己决定实例化哪一个工厂类，工厂模式使其创建过程延迟到子类进行。

![](/Users/sunxu/mycode/Java-notes/notes/pictures/java-design-patterns/Snipaste_2020-02-23_16-13-52.png)

在工厂模式中，我们在创建对象时不会对客户端暴露创建逻辑，并且是通过使用一个共同的接口来指向新创建的对象。

Spring使用工厂模式可以通过 `BeanFactory` 或 `ApplicationContext` 创建 bean 对象。简单工厂模式的实质是由一个工厂类根据传入的参数，动态决定应该创建哪一个产品类。 spring中的BeanFactory就是简单工厂模式的体现，根据传入一个唯一的标识来获得bean对象，但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。

**两者对比：**

- `BeanFactory` ：延迟注入(使用到某个 bean 的时候才会注入),相比于`BeanFactory` 来说会占用更少的内存，程序启动速度更快。
- `ApplicationContext` ：容器启动的时候，不管你用没用到，一次性创建所有 bean 。`BeanFactory` 仅提供了最基本的依赖注入支持，`ApplicationContext` 扩展了 `BeanFactory` ,除了有`BeanFactory`的功能还有额外更多功能，所以一般开发人员使用`ApplicationContext`会更多。

## 单例模式（Singleton）

**Spring 中 bean 的默认作用域就是 singleton(单例)的。** 除了 singleton 作用域，Spring 中 bean 还有下面几种作用域：

- prototype : 每次请求都会创建一个新的 bean 实例。
- request : 每一次HTTP请求都会产生一个新的bean，该bean仅在当前HTTP request内有效。
- session : 每一次HTTP请求都会产生一个新的 bean，该bean仅在当前 HTTP session 内有效。
- global-session： 全局session作用域，仅仅在基于portlet的web应用中才有意义，Spring5已经没有了。Portlet是能够生成语义代码(例如：HTML)片段的小型Java Web插件。它们基于portlet容器，可以像servlet一样处理HTTP请求。但是，与 servlet 不同，每个 portlet 都有不同的会话

**Spring 实现单例的方式：**

- xml : `<bean id="userService" class="top.snailclimb.UserService" scope="singleton"/>`
- 注解：`@Scope(value = "singleton")`

**Spring 通过 `ConcurrentHashMap` 实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码如下**

```
// 通过 ConcurrentHashMap（线程安全） 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "'beanName' must not be null");
        synchronized (this.singletonObjects) {
            // 检查缓存中是否存在实例
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                //...省略了很多代码
                try {
                    singletonObject = singletonFactory.getObject();
                }
                //...省略了很多代码
                // 如果实例对象在不存在，我们注册到单例注册表中。
                addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
        }
    }
    //将对象添加到单例注册表
    protected void addSingleton(String beanName, Object singletonObject) {
            synchronized (this.singletonObjects) {
                this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

            }
        }
}
```

## 代理设计模式

### 代理模式在 AOP 中的应用

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关，**却为业务模块所共同调用的逻辑或责任（例如事务处理、日志管理、权限控制等）封装起来**，便于**减少系统的重复代码**，**降低模块间的耦合度**，并**有利于未来的可拓展性和可维护性**。

**Spring AOP 就是基于动态代理的**，如果要代理的对象，实现了某个接口，那么Spring AOP会使用**JDK Proxy**，去创建代理对象，而对于没有实现接口的对象，就无法使用 JDK Proxy 去进行代理了，这时候Spring AOP会使用**Cglib** ，这时候Spring AOP会使用 **Cglib** 生成一个被代理对象的子类来作为代理

## 适配器模式

适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口，适配器模式使接口不兼容的那些类可以一起工作，其别名为包装器(Wrapper)。

### spring AOP中的适配器模式

我们知道 Spring AOP 的实现是基于代理模式，但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式，与之相关的接口是`AdvisorAdapter` 。Advice 常用的类型有：`BeforeAdvice`（目标方法调用前,前置通知）、`AfterAdvice`（目标方法调用后,后置通知）、`AfterReturningAdvice`(目标方法执行结束后，return之前)等等。每个类型Advice（通知）都有对应的拦截器:`MethodBeforeAdviceInterceptor`、`AfterReturningAdviceAdapter`、`AfterReturningAdviceInterceptor`。Spring预定义的通知要通过对应的适配器，适配成 `MethodInterceptor`接口(方法拦截器)类型的对象（如：`MethodBeforeAdviceInterceptor` 负责适配 `MethodBeforeAdvice`）。

### spring MVC中的适配器模式

在Spring MVC中，`DispatcherServlet` 根据请求信息调用 `HandlerMapping`，解析请求对应的 `Handler`。解析到对应的 `Handler`（也就是我们平常说的 `Controller` 控制器）后，开始由`HandlerAdapter` 适配器处理。`HandlerAdapter` 作为期望接口，具体的适配器实现类用于对目标类进行适配，`Controller` 作为需要适配的类。

**为什么要在 Spring MVC 中使用适配器模式？** Spring MVC 中的 `Controller` 种类众多，不同类型的 `Controller` 通过不同的方法来对请求进行处理。如果不利用适配器模式的话，`DispatcherServlet` 直接获取对应类型的 `Controller`，需要的自行来判断，像下面这段代码一样：

```
if(mappedHandler.getHandler() instanceof MultiActionController){
   ((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
    ...
}else if(...){
   ...
}
复制代码
```

假如我们再增加一个 `Controller`类型就要在上面代码中再加入一行 判断语句，这种形式就使得程序难以维护，也违反了设计模式中的开闭原则 – 对扩展开放，对修改关闭。

## 装饰者模式

装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承，装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能，但我们又不愿直接去修改原有的代码时，设计一个Decorator套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式，比如 `InputStream`家族，`InputStream` 类下有 `FileInputStream` (读取文件)、`BufferedInputStream` (增加缓存,使读取文件速度大大提升)等子类都在不修改`InputStream` 代码的情况下扩展了它的功能。

## 责任链模式

监听器 过滤器.

或者打印日志.<file_sep>/codes/javacode/designpattern/Singleton.java
package javacode.designpattern;

/**
 * @author <EMAIL>
 * @date 19/9/15/015 21:45
 */
public class Singleton {

    //懒汉线程不安全
//    private static Singleton singletonInstance;
//
//    private Singleton() {
//
//    }
//
//    public static Singleton getUniqueInstance() {
//        if (singletonInstance == null) {
//            singletonInstance = new Singleton();
//        }
//        return singletonInstance;
//    }

    //饿汉式
//    private Singleton() {
//
//    }
//    private static Singleton singletonInstance = new Singleton();

}
<file_sep>/codes/javacode/jichu/String2Double.java
package javacode.jichu;

import com.sun.deploy.util.StringUtils;

/**
 * @author <EMAIL>
 * @date 19/9/16/016 15:15
 */
public class String2Double {
    public static  Double func(String string) {
        //缺少了一种判定条件  "".equals()  直接用 isEmpty()代替
        // trim把头尾的空格去掉 然后返回值
        if (string==null||"".equals(string.trim())) {
            return null;
        }
        Double sum = 0.0;
        String[] array = string.split("\\.");
        //十位部分
        String str1 = array[0];
        Double wei = 1.0;
        for (int i = str1.length() - 1; i >= 0; i--) {
            int num = str1.charAt(i) - '0';
            sum = num * wei + sum;
            wei *= 10;
        }
        //小数部分
        String str2 = array[1];
        wei = 0.1;
        for (int i = 0; i < str2.length(); i++) {
            int num = str2.charAt(i) - '0';
            sum = num * wei + sum;
            wei *= 0.1;
        }
        return sum;
    }

    public static void main(String[] args) {
        Double sum = func("0.1");
        System.out.println(sum);
    }
}
<file_sep>/notes/SQL引擎.md
# 星型模型和雪花型模型比较

https://blog.csdn.net/nisjlvhudy/article/details/7889422

# HBase 上的 SQL 引擎，Kylin 和 Phoenix 的不同

https://www.cnblogs.com/bonelee/p/12442904.html

1）Kylin 和 Phoenix 虽然同为 Hadoop/HBase 上的 SQL 引擎，两者的定位不同，一个是 OLAP，另一个是 OLTP，服务于不同的场景；

2）Phoenix 更多的是适用于以往关系型数据库的相关操作，当查询语句是点查找和小范围扫描时，Phoenix 可以比较好地满足，而它不太适合大量 scan 类型的 OLAP 查询，或查询的模式较为灵活的场景；

3）Kylin 是一个只读型的分析引擎，不适合细粒度修改数据，但适合做海量数据的交互式在线分析，通常跟数据仓库以及 BI 工具结合使用，目标用户为业务分析人员。

# OLAP、OLTP的介绍和比较

https://blog.csdn.net/zhangzheng0413/article/details/8271322

数据处理大致可以分成两大类：联机事务处理OLTP（on-line transaction processing）、联机分析处理OLAP（On-Line Analytical Processing）。OLTP是传统的关系型数据库的主要应用，主要是基本的、日常的事务处理，例如银行交易。OLAP是数据仓库系统的主要应用，支持复杂的分析操作，侧重决策支持，并且提供直观易懂的查询结果。

# MySQL存储引擎

## MySQL逻辑架构

MySQL是一个开放源代码的关系数据库管理系统。原开发者为瑞典的MySQL AB公司，最早是在2001年MySQL3.23进入到管理员的视野并在之后获得广泛的应用。

当MySQL启动（MySQL服务器就是一个进程），等待客户端连接，每一个客户端连接请求，服务器都会新建一个线程处理（如果是线程池的话，则是分配一个空的线程），每个线程独立，拥有各自的内存处理空间。

MySQL总体上可分为Server层和存储引擎层。

Server层包括连接器、查询器、分析器、优化器、执行器等，涵盖 MySQL 的大多数核心服务功能，以及所有的内置函数（如日期、时间、数学和加密函数等），所有跨存储引擎的功能都在这一层实现，比如存储过程、触发器、视图等。

存储引擎层负责数据的存储和提取。其架构模式是插件式的，支持 InnoDB、MyISAM、Memory 等多个存储引擎。

MySQL 整体上可以分为 Server 层和存储引擎层两部分。详细的分层如下：

1.客户端层： 连接处理、授权认证、安全等功能均在这一层处理。包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcp/ip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念，为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。

2.核心服务层：查询解析、分析、优化、缓存、内置函数(比如：时间、数学、加密等函数)等。该层架构主要完成核心服务功能，如SQL接口，并完成缓存的查询，SQL的分析和优化及部分内置函数的执行。所有跨存储引擎的功能也在这一层实现，如过程、函数等。在该层，服务器会解析查询并创建相应的内部解析树，并对其完成相应的优化如确定查询表的顺序，是否利用索引等，最后生成相应的执行操作。如果是select语句，服务器还会查询内部的缓存。如果缓存空间足够大，这样在解决大量读操作的环境中能够很好的提升系统的性能。

3.存储引擎层：存储过程、触发器、视图等。存储引擎真正的负责了MySQL中数据的存储和提取，服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同，这样我们可以根据自己的实际需要进行选取。

4.数据存储层，主要是将数据存储在运行于裸设备的文件系统之上，并完成与存储引擎的交互。

最下层为存储引擎，其负责MySQL中的数据存储和提取。和Linux下的文件系统类似，每种存储引擎都有其优势和劣势。中间的服务层通过API与存储引擎通信，这些API接口屏蔽了不同存储引擎间的差异。

## MySQL查询过程

我们总是希望MySQL能够获得更高的查询性能，最好的办法是弄清楚MySQL是如何优化和执行查询的。一旦理解了这一点，就会发现：**很多的查询优化工作实际上就是遵循一些原则让MySQL的优化器能够按照预想的合理方式运行而已。**比如向左解析，向右解析；比如连接池。

当向MySQL发送一个请求的时候，MySQL到底做了些什么呢？

## 客户端/服务端通信协议

MySQL客户端/服务端通信协议是“半双工”的：在任一时刻，要么是服务器向客户端发送数据，要么是客户端向服务器发送数据，这两个动作不能同时发生。一旦一端开始发送消息，另一端要接收完整个消息才能响应它，所以我们无法也无须将一个消息切成小块独立发送，也没有办法进行流量控制。

客户端用一个单独的数据包将查询请求发送给服务器，所以当查询语句很长的时候，需要设置max_allowed_packet参数。但是需要注意的是，如果查询实在是太大，服务端会拒绝接收更多数据并抛出异常。

与之相反的是，服务器响应给用户的数据通常会很多，由多个数据包组成。但是当服务器响应客户端请求时，客户端必须完整的接收整个返回结果，而不能简单的只取前面几条结果，然后让服务器停止发送。因而在实际开发中，尽量保持查询简单且只返回必需的数据，减小通信间数据包的大小和数量是一个非常好的习惯，这也是查询中尽量避免使用SELECT *以及加上LIMIT限制的原因之一。

## SQL缓存

在解析一个查询语句前，如果查询缓存是打开的，那么MySQL会检查这个查询语句是否命中查询缓存中的数据。如果当前查询恰好命中查询缓存，在检查一次用户权限后直接返回缓存中的结果。这种情况下，查询不会被解析，也不会生成执行计划，更不会执行。

MySQL将缓存存放在一个引用表（不要理解成table，可以认为是类似于HashMap的数据结构），通过一个哈希值索引，这个哈希值通过查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来。所以两个查询在任何字符上的不同（例如：空格、注释），都会导致缓存不会命中。

如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表，其查询结果都不会被缓存。比如函数NOW()或者CURRENT_DATE()会因为不同的查询时间，返回不同的查询结果，再比如包含CURRENT_USER或者CONNECION_ID()的查询语句会因为不同的用户而返回不同的结果，将这样的查询结果缓存起来没有任何的意义。

既然是缓存，就会失效，那查询缓存何时失效呢？MySQL的查询缓存系统会跟踪查询中涉及的每个表，如果这些表（数据或结构）发生变化，那么和这张表相关的所有缓存数据都将失效。正因为如此，在任何的写操作时，MySQL必须将对应表的所有缓存都设置为失效。如果查询缓存非常大或者碎片很多，这个操作就可能带来很大的系统消耗，甚至导致系统僵死一会儿。而且查询缓存对系统的额外消耗也不仅仅在写操作，读操作也不例外：

1. 任何的查询语句在开始之前都必须经过检查，即使这条SQL语句永远不会命中缓存
2. 如果查询结果可以被缓存，那么执行完成后，会将结果存入缓存，也会带来额外的系统消耗

基于此，我们要知道并不是什么情况下查询缓存都会提高系统性能，缓存和失效都会带来额外消耗，只有当缓存带来的资源节约大于其本身消耗的资源时，才会给系统带来性能提升。但要如何评估打开缓存是否能够带来性能提升是一件非常困难的事情，也不在本文讨论的范畴内。如果系统确实存在一些性能问题，可以尝试打开查询缓存，并在数据库设计上做一些优化，比如：

1. 用多个小表代替一个大表，注意不要过度设计
2. 批量插入代替循环单条插入
3. 合理控制缓存空间大小，一般来说其大小设置为几十兆比较合适
4. 可以通过SQL_CACHE和SQL_NO_CACHE来控制某个查询语句是否需要进行缓存

最后的忠告是不要轻易打开查询缓存，特别是写密集型应用。如果你实在是忍不住，可以将query_cache_type设置为DEMAND，这时只有加入SQL_CACHE的查询才会走缓存，其他查询则不会，这样可以非常自由地控制哪些查询需要被缓存。

当然查询缓存系统本身是非常复杂的，这里讨论的也只是很小的一部分，其他更深入的话题，比如：缓存是如何使用内存的？如何控制内存的碎片化？事务对查询缓存有何影响等等

## 语法解析和预处理

MySQL通过关键字将SQL语句进行解析，并生成一颗对应的解析树。

这个过程解析器主要通过语法规则来验证和解析。比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等。

**SQL执行计划流程图**

## 查询优化

经过前面的步骤生成的语法树被认为是合法的了，并且由优化器将其转化成查询计划。多数情况下，一条查询可以有很多种执行方式，最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。

MySQL使用基于成本的优化器，它尝试预测一个查询使用某种执行计划时的成本，并选择其中成本最小的一个。在MySQL可以通过查询当前会话的last_query_cost的值来得到其计算当前查询的成本。

## 事务

- 共享锁与排它锁
    -  读
        - 共享锁
    -  写
        - 排它锁
-  隔离怎么实现？
    - 锁
    -  InnoDB存在两种锁
        - 共享锁（S锁） ：（插入/修改/删除）资源获取S锁之后，能加S锁，不能加X锁
        - 排它锁（X锁） ： 资源加上X锁之后，不能加S锁，也不能加X锁

## 数据库事务实现原理剖析

### 事务的实现原理

-  事务的执行过程
    - 系统会为每个事务开辟一个私有工作区
    - 事务读操作将从磁盘中拷贝数据项到工作区中,在执行写操作前所有的更新都作用于工作区中的拷贝.
    - 事务的写操作将把数据输出到内存的缓冲区中,再由缓冲区管理器将数据写入到磁盘。
-  分特性
    -  原子性
        -  原子性是事务的基本特性，保证了事务中的操作是不可拆分的整体，那么原子性是如何实现的呢？事务的原子性表现的两个方面：
            -  事务提交成功时，那么事务中的操作总会完成
                - 事务提交成功保证事务中的操作都会完成。1、是正确执行完事务，没有出现任何问题；2、是事务提交成功但是出异常，数据库恢复之后，提交完成的事务会保证数据库完成该事物的操作。对于第一种正常情况不予讨论，因为不存在 异常情况，那么第2种实际上是和上文说的持久性是相关联的，而这个是基于重做日志（redo log）来保证提交完成的事务在异常情况下保证数据操作能够进行：
            -  事务提交失败，那么事务中的操作都失败
                - 事务提交失败，那么事务中的操作都失败，这个是通过数据库的撤销操作日志来保证的，也称之为undo log。
        - 通过redo log 来实现
    -  一致性
        - 通过undo log 来实现
    -  隔离性
        - 锁
        -  MVCC 多版本并发控制
            -  MVCC 是什么？
                - 人们一般把基于锁的并发控制机制称成为悲观机制，而把MVCC机制称为乐观机制。这是因为锁机制是一种预防性的，读会阻塞写，写也会阻塞读，当锁定粒度较大，时间较长时并发性能就不会太好；而MVCC是一种后验性的，读不阻塞写，写也不阻塞读，等到提交的时候才检验是否有冲突，由于没有锁，所以读写不会相互阻塞，从而大大提升了并发性能。
                -  通过增加系统版本号，每次事务操作，会比较系统版本号
                - InnoDB为每行记录添加了一个版本号（系统版本号），每当修改数据时，版本号加一。在读取事务开始时，系统会给事务一个当前版本号，事务会读取版本号<=当前版本号的数据，这时就算另一个事务插入一个数据，并立马提交，新插入这条数据的版本号会比读取事务的版本号高，因此读取事务读的数据还是不会变。
            - 基于CAS（Compare-and-swap）
            - 有条件更新（Conditional Update）
-  Undo原理 与 Redo原理
    -  Undo原理：（备份旧数据）
        - 在操作任何数据之前，首先将数据备份到一个地方（这个存储数据备份的地方称为Undo Log）。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句，系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。
    -  Redo原理：（保存最新数据）
        - 和Undo Log相反，Redo Log记录的是新数据的备份。在事务提交前，只要将Redo Log持久化即可，不需要将数据持久化。当系统崩溃时，虽然数据没有持久化，但是Redo Log已经持久化。系统可以根据Redo Log的内容，将所有数据恢复到最新的状态。

# 分布式sql引擎原理分析

#### SQL解析器

当用户通过客户端向服务端提交查询后，SQL首先会来到SQL解析器，SQL解析器负责对这条文本形式的SQL进行词法分析、语法分析。通常我们会通过成熟的“解析器生成器”来生成面向特定语法的解析器的核心代码，而不会从头开始手写整个SQL解析器，因为这并不是一件简单的事情，也没必要这么做。比较常见的“解析器生成器”有Antlr和JavaCC，而后者主要面向Java用户。由于Drill SQL部分的相关功能重度依赖于一个独立的项目Apache Calcite（这部分内容详见《浅析查询优化器》），而Calcite使用的便是JavaCC。为了能通过JavaCC生成解析器，用户需要按照JavaCC的规范编写特定语法的文法文件，有兴趣的同学可以参考JavaCC官网。经过SQL解析器的处理后，SQL文本被转化成了一棵由SqlNode组成的抽象语法树（AST）。

#### 语义分析器

抽象语法树是完全基于文法结构与对文本内容解析生成的，但是从语义的角度来看这样的组合未必是合理的。举个简单的例子，我们可以用主谓宾这样的结构来分解句子：”I play basketball“这个句子符合主谓宾结构的，而”I eat basketball“这个句子也符合，他们都通过了语法分析及检查。但是从语义的角度看，后者明显是不合常理的。这种“不合常理”就需要语义分析器来发现，而对于合理的节点则需要进一步赋予其更多的有效信息。在对SQL的语义分析中，我们主要会进行语法树节点的检查和重写。举个例子，检查部分会去判断SQL中引用的表名、列名、函数名是否存在、有效。对某些表达式进行有效性验证，比如“+“两边的数据类型是否是可互加的。传给函数的入参的类型列表是否能匹配上该函数的签名。对字面量的值进行范围检查，以防止出现非法值或越界。同时，这个过程还会做一些节点重写，比如为某些场景做一些节点的隐式类型转换，遇到视图节点时会将其展开，目的是能让这棵语法树变得更加接地气，成为一棵具体语法树。该具体语法树实质上是一个查询路径的关系代数表示，我们将其叶子节点称之为关系；而内部节点就是对这些关系的运算符，我们通常称之为逻辑算子。

#### 计划器（优化器）

正如上文所述，语义分析最终的产物是一个查询路径的关系代数表示，这其实已经是一个初步的逻辑执行计划了。而基于关系代数的等价转换，我们可以获得很多种查询路径。那么如何才能找到成本较低的查询路径呢。这就是计划器的第一项优化职责，即逻辑执行计划优化。这一阶段，我们往往会基于一些特定的经验和直观的感受来编写规则，并采用启发式策略来匹配和应用这些规则，初步逻辑执行计划在这些规则的应用下得到一定程度的优化，从而产出了较优的逻辑执行计划。接着，我们需要结合实际的执行引擎和系统实现来将逻辑执行计划转换成可以在特定运行时环境运行的物理执行计划。此时我们就需要为Join算子选择合适的Join算法实现，并为具有分布式特质的实现加入跨机器的Exchange算子等。在这个环节中，我们可以结合统计信息，来对所有具体的物理操作进行成本估算，从而来进一步对物理执行计划进行优化，这就是计划器的第二项优化职责，即物理执行计划优化。Drill的这部分实现重度依赖于Calcite的CBO能力，并为此提供了符合Calcite框架的大量规则。从而确保最终能在合理的时间内尽可能缩小搜索空间，产出较优的物理执行计划（这部分内容详见《浅析查询优化器》）。

#### 运行时（Runtime）

当我们拿到物理执行计划之后，便可以开始去真正执行计算作业了。运行时是用于完成物理执行计划执行工作的组件及环境，我们需要一套环境来运行上述步骤生成的物理执行计划。那么本文讨论的主角就此上场了。

说到这里，SQL查询引擎的关键组件及其脉络已经介绍完了，那么我们来看看传统运行时的一些问题以及对应的改善手段。

<file_sep>/notes/常考算法.md

- [排序](#排序)
    - [快速排序](#快速排序)
- [单例](#单例)
- [非递归树](#非递归树)
- [top k](#top-k)
    - [1. Kth Element](#1-kth-element)
- [层序遍历二叉树](#层序遍历二叉树)
- [非递归先序遍历二叉](#非递归先序遍历二叉)
- [String 与 char[]数组](#string-与-char数组)
    - [相互转换](#相互转换)
    - [String的常用方法](#string的常用方法)
    - [数组](#数组)
- [Array与数组  相互转化](#array与数组--相互转化)
    - [Array有哪些方法](#array有哪些方法)
    - [数组](#数组-1)

# 排序

## 快速排序

# 单例

# 非递归树

# top k

## 1. Kth Element

[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/)

```text
Input: [3,2,1,5,6,4] and k = 2
Output: 5
```

题目描述：找到倒数第 k 个的元素。

**排序** ：时间复杂度 O(NlogN)，空间复杂度 O(1)

```java
public int findKthLargest(int[] nums, int k) {
    Arrays.sort(nums);
    return nums[nums.length - k];
}
```

**堆** ：时间复杂度 O(NlogK)，空间复杂度 O(K)。

```java
public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小顶堆
    for (int val : nums) {
        //把所有数据都放一边,每次用poll取出取出二叉树中最小的值,
        //最后用peek()返回第k小的数
        pq.add(val);
        if (pq.size() > k)  // 维护堆的大小为 K
            pq.poll();
    }
    return pq.peek();
}
```

**快速选择** ：时间复杂度 O(N)，空间复杂度 O(1)

```java
public int findKthLargest(int[] nums, int k) {
    k = nums.length - k;
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int j = partition(nums, l, h);
        if (j == k) {
            break;
        } else if (j < k) {
            l = j + 1;
        } else {
            h = j - 1;
        }
    }
    return nums[k];
}

private int partition(int[] a, int l, int h) {
    //h+1是因为:i=l,而l处的值是中分线,所以i和j最初的值本来应该是
    //i= l+1,j = h+1-1 为了下面*处的代码对称,所以给用"int i = l, j = h + 1;"
    int i = l, j = h + 1;
    while (true) {
        while (a[++i] < a[l] && i < h) ;
        while (a[--j] > a[l] && j > l) ;
        if (i >= j) {
            break;
        }
        swap(a, i, j);
    }
    swap(a, l, j);
    return j;
}

private void swap(int[] a, int i, int j) {
    int t = a[i];
    a[i] = a[j];
    a[j] = t;
}
```
# 层序遍历二叉树

```java
import java.util.LinkedList;

public class LevelOrder
{
  public void levelIterator(BiTree root)
  {
	  if(root == null)
	  {
		  return ;
	  }
	  LinkedList<BiTree> queue = new LinkedList<BiTree>();
	  BiTree current = null;
	  queue.offer(root);//将根节点入队
	  while(!queue.isEmpty())
	  {
		  current = queue.poll();//出队队头元素并访问
		  System.out.print(current.val +"-->");
		  if(current.left != null)//如果当前节点的左节点不为空入队
		  {
			  queue.offer(current.left);
		  }
		  if(current.right != null)//如果当前节点的右节点不为空，把右节点入队
		  {
			  queue.offer(current.right);
		  }
	  }

  }

}
```

# 非递归先序遍历二叉
```java
     * */
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> resultList=new ArrayList<>();
        Stack<TreeNode> treeStack=new Stack<>();
        if(root==null) //如果为空树则返回
            return resultList;
        treeStack.push(root);
        while(!treeStack.isEmpty()){
            TreeNode tempNode=treeStack.pop();
            if(tempNode!=null){
                resultList.add(tempNode.val);//访问根节点
                treeStack.push(tempNode.right); //入栈右孩子
                treeStack.push(tempNode.left);//入栈左孩子
            }
        }
        return resultList;
    }
```

冒泡,归并,插入,基数排序是稳定的排序算法

(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较，交换也发生在这两个元素之间。所以，如果两个元素相等，我想你是不会再无聊地把他们俩交换一下的；如果两个相等的元素没有相邻，那么即使通过前面的两两交换把两个相邻起来，这时候也不会交换，所以相同元素的前后顺序并没有改 变，所以冒泡排序是一种稳定排序算法。

(2)选择排序
选择排序是给每个位置选择当前元素最小的，比如给第一个位置选择最小的，在剩余元素里面给第二个元素选择第二小的，依次类推，直到第n-1个元素，第n个 元素不用选择了，因为只剩下它一个最大的元素了。那么，在一趟选择，如果当前元素比一个元素小，而该小的元素又出现在一个和当前元素相等的元素后面，那么 交换后稳定性就被破坏了。比较拗口，举个例子，序列5 8 5 2 9， 我们知道第一遍选择第1个元素5会和2交换，那么原序列中2个5的相对前后顺序就被破坏了，所以选择排序不是一个稳定的排序算法。

(3)插入排序
插入排序是在一个已经有序的小序列的基础上，一次插入一个元素。当然，刚开始这个有序的小序列只有1个元素，就是第一个元素。比较是从有序序列的末尾开 始，也就是想要插入的元素和已经有序的最大者开始比起，如果比它大则直接插入在其后面，否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的，那么插入元素把想插入的元素放在相等元素的后面。所以，相等元素的前后顺序没有改变，从原无序序列出去的顺序就是排好序后的顺序，所以插入排序是稳 定的。

(4)快速排序
快速排序有两个方向，左边的i下标一直往右走，当a[i] <= a[center_index]，其中center_index是中枢元素的数组下标，一般取为数组第0个元素。而右边的j下标一直往左走，当a[j] > a[center_index]。如果i和j都走不动了，i <= j, 交换a[i]和a[j],重复上面的过程，直到i>j。 交换a[j]和a[center_index]，完成一趟快速排序。在中枢元素和a[j]交换的时候，很有可能把前面的元素的稳定性打乱，比如序列为 5 3 3 4 3 8 9 10 11， 现在中枢元素5和3(第5个元素，下标从1开始计)交换就会把元素3的稳定性打乱，所以快速排序是一个不稳定的排序算法，不稳定发生在中枢元素和a[j] 交换的时刻。

(5)归并排序
归并排序是把序列递归地分成短序列，递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列，不断合并直到原序列全部排好序。可以发现，在1个或2个元素时，1个元素不会交换，2个元素如果大小相等也没有人故意交换，这不会破坏稳定 性。那么，在短的有序序列合并的过程中，稳定是否受到破坏？没有，合并过程中我们可以保证如果两个当前元素相等时，我们把处在前面的序列的元素保存在结 果序列的前面，这样就保证了稳定性。所以，归并排序也是稳定的排序算法。

(6)基数排序
基数排序是按照低位先排序，然后收集；再按照高位排序，然后再收集；依次类推，直到最高位。有时候有些属性是有优先级顺序的，先按低优先级排序，再按高优 先级排序，最后的次序就是高优先级高的在前，高优先级相同的低优先级高的在前。基数排序基于分别排序，分别收集，所以其是稳定的排序算法。

(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序，当刚开始元素很无序的时候，步长最大，所以插入排序的元素个数很少，速度很快；当元素基本有序了，步长很小， 插入排序对于有序的序列效率很高。所以，希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序，我们知道一次插入排序是稳定的，不会改变相同元 素的相对顺序，但在不同的插入排序过程中，相同的元素可能在各自的插入排序中移动，最后其稳定性就会被打乱，所以shell排序是不稳定的。

(8)堆排序
我们知道堆的结构是节点i的孩子为2*i和2*i+1节点，大顶堆要求父节点大于等于其2个子节点，小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列，堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, ...1这些个父节点选择元素时，就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了，而第n/2-1个父节点把后面一个相同的元素没 有交换，那么这2个相同的元素之间的稳定性就被破坏了。所以，堆排序不是稳定的排序算法。
综上，得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法，而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。

# String 与 char[]数组

## 相互转换
char[] 转  String

1.直接在构造String时建立。 char data[] = {'s', 'g', 'k'}; String str = new String(data);

2.String有方法可以直接转换。 String.valueOf(char[] chr)就可以。 如： char[] cha = {'s','g','h'}; String n = String.valueOf(cha);

String 转char[]

string.toCharArray()

## String的常用方法

toCharArray()
charAt(int index)

trim()
isEmpty()  "" 但是不判断是否为null
length()
subString(int start ,int end)  左闭右开
split(String regex)   正则表达
indexOf(char ch)
contains(CharSequence s)
replace(char old,char new)
toLowerCase()
toUpperCase()
int  compareTo(String )  找到第一个对应位置不相等的字符,并**返回字符的差值**
int  compareToIngnoreCase(String)

## 数组
只有 a.length ;好处是可以下标操作(String的下标操作用的是charAt(int index))

# Array与数组  相互转化

## Array有哪些方法

## 数组

<file_sep>/notes/LeetCode04.md

- [[@@@@523. 连续的子数组和](https://leetcode-cn.com/problems/continuous-subarray-sum/)](#523-连续的子数组和httpsleetcode-cncomproblemscontinuous-subarray-sum)
- [[@@@@974. 和可被 K 整除的子数组](https://leetcode-cn.com/problems/subarray-sums-divisible-by-k/)](#974-和可被-k-整除的子数组httpsleetcode-cncomproblemssubarray-sums-divisible-by-k)
- [剔除k个数求最长连续区间](#剔除k个数求最长连续区间)

#### [@@@@523. 连续的子数组和](https://leetcode-cn.com/problems/continuous-subarray-sum/)

@@@map存前缀和(map.get(i) 拿到的是-1~i的sum,-1的sum为0)的思路,可以用来解决求区间和的问题.

另外还有如下性质, 举例  i=1处,对5取模为3   j= 4处对5取模为3.则 index在2~4处的和可以整除5

@@为了处理case  [0,0]  k=0.而设置的map.put(0,-1)  这个哨兵太厉害了!

@@ i-map.get(sum)>=2  当前sum的index  减去之前sum的index是区间的长度.而非长度-1.

@@为了处理k=0,而设置的  if(k!=0)sum = sum%k;   这样的话,在k为0的时候,sum就是求阶梯和,如果存在map.contains(sum).则这段区间和为0

@@在k!=0的情况下,map记录sum的取余之后的值,找到map里包含sum,而且index比当前i小0以上的坐标

```java
public boolean checkSubarraySum(int[] nums, int k) {
    HashMap<Integer, Integer> map = new HashMap<>();
    int sum = 0;
    map.put(0, -1);
    for (int i = 0; i < nums.length; i++) {
        sum += nums[i];
        if (k != 0) {
            sum = sum % k;
        }
        if (map.containsKey(sum)) {
            if (i - map.get(sum) > 1) {
                return true;
            }
        } else {
            map.put(sum, i);
        }
    }
    return false;
}
```

#### [@@@@974. 和可被 K 整除的子数组](https://leetcode-cn.com/problems/subarray-sums-divisible-by-k/)

```java
思路1 : 错误的思路: 因为n过大,二维数组dp[i][j]存i~j的和会内存超出,而且时间复杂度也很高.

思路2:前缀和
    @@ 和为0的哨兵
    @@ nums[i]可能是过于大的整数或者负数, 所以要 先取余k,再加k,再取余sum = (sum % k + k) % k;
	@@故若想(sum[j]−sum[i])%K=0，则必有sum[j]%K = sum[i]%K .所以中途记录之前的sum和sum出现的次数.
        (如果要求得是区间长度等信息,则记录sum和sum出现的最早index)

public int subarraysDivByK(int[] nums, int k) {
        HashMap<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        int sum = 0;
        int ret = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            sum = (sum % k + k) % k;
            if (map.containsKey(sum)) {
                ret += map.get(sum);
            }
            map.put(sum, map.getOrDefault(sum, 0) + 1);
        }
        return ret;
    }

```

#### @@@抽奖

题目:有1000人参与抽奖,10个奖品,每个人的抽奖次数可能是1到80次,设计一个抽奖算法,最后要把奖品抽完

假设一共抽奖n次，当前还剩k个奖品，第i次调用时，中奖概率 k/(n+1-i)

1.可以保证每个人的概率相等(第一个人的中奖概率是 10/1000,第二个人的中奖概率分两部分,第一个人中奖和不中奖)

2.可以保证最后抽完(k过大,会让中奖概率大于1)

#### @@完全二叉树的节点个数

完全二叉树

O

|\
OO

|\ |\

OO OO

|\ |\ |\ |\

OO Ox xx xx

\----------

求总的节点数？

```java

public class Main {

    private int func(TreeNode root) {
        if (root == null) {
            return 0;
        }
        TreeNode node = root;
        int maxLevel = 0;
        int sum = 0;
        while (node != null) {
            node = node.left;
            maxLevel++;
        }

        Node rootNode = new Node(1, root, 1);
        Node tempNode = null;
        while (rootNode.level <= maxLevel - 1) {
            tempNode = findMid(rootNode);
            if (maxLevel > tempNode.level) {
                rootNode = new Node(rootNode.position * 2 - 1, rootNode.node.left, rootNode.level + 1);
            } else if (maxLevel == tempNode.level) {
                rootNode = new Node(rootNode.position * 2, rootNode.node.right, rootNode.level + 1);
            }
        }
        sum = (int) Math.pow(2, maxLevel - 1) - 1 + tempNode.position;
        return sum;
    }

    private Node findMid(Node root) {
        if (root == null) {
            return null;
        }
        TreeNode treeNode = root.node.left;
        int level = root.level;
        int position = root.position;

        if (treeNode == null) {
            return root;
        }
        while (treeNode.right != null) {
            treeNode = treeNode.right;
            level++;
            position = position * 2;
        }
        return new Node(position, treeNode, level);
    }
}

class Node {
    int position;
    TreeNode node;
    int level;

    public Node(int positon, TreeNode node, int level) {
        this.position = position;
        this.node = node;
        this.level = level;
    }
}

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
}
```

#### 寻找最左边的1

给出01矩阵，每一行左边是0，右边是1，找到起始1的位置在最左边的1的下标。

如题

0000111

0000011

0001111

0111111

000001

返回第四行，下标为1.

思路1：逐列遍历，

思路2：利用类似于跳表的性质

第一行，折半查找，找到最左边的1所在的位置（colIndex = 4），然后跳到下一行

第二行，如果第二行的colIndex=4的地方为0，则往第三行跳。如果为1，则利用折半查找，找到第二行最左边1的坐标，再往第三行跳

#### 牛奶试毒

1000桶牛奶，1桶有毒，用10只小白鼠试出来。其实就是二进制，网上有答案，提示：2^10 = 1024.

```
链接：https://www.nowcoder.com/questionTerminal/84726a200b4a4a0d95b9565b4c6eb216
来源：牛客网

每个老鼠只有死或活2种状态，因此每个老鼠可以看作一个bit，取0或1
N个老鼠可以看作N个bit，可以表达2^N种状态（其中第i个状态代表第i个瓶子有毒）

例如：当N＝2时，可以表达4种状态
0，0（ 一号老鼠活，二号老鼠活）
0，1（ 一号老鼠活，二号老鼠死）
1，0（ 一号老鼠死，二号老鼠活）
1，1（ 一号老鼠死，二号老鼠死）
具体来说，有A、B、C、D这4个瓶子，一号老鼠喝C和D(因为第一位为1的两个状态时CD)， 二号老鼠喝B和D(因为第二位为1的两个状态是BD)
如果 0，0 （ 一号老鼠活，二号老鼠活），说明是A有毒
如果 0，1 （ 一号老鼠活，二号老鼠死） ，说明是B有毒
如果 1，0 （ 一号老鼠死，二号老鼠活） ，说明是C有毒
如果 1，1 （ 一号老鼠死，二号老鼠死） ，说明是D有毒

解析：

给1000个瓶分别标上如下标签（10位长度）：
0000000001 （第1瓶）
0000000010 （第2瓶）
0000000011 （第3瓶）
.
1111101000 （第1000瓶）
从编号最后1位是1的所有的瓶子里面取出1滴混在一起（比如从第一瓶,第三瓶,.里分别取出一滴混在一起）并标上记号为1.给第一个小白鼠喝。
从编号倒数第2位是1的所有的瓶子里面取出1滴混在一起并标上记号为2.给第2个小白鼠喝。
从编号倒数第3位是1的所有的瓶子里面取出1滴混在一起并标上记号为3.给第3个小白鼠喝。
从编号倒数第4位是1的所有的瓶子里面取出1滴混在一起并标上记号为4.给第4个小白鼠喝。
从编号倒数第5位是1的所有的瓶子里面取出1滴混在一起并标上记号为5.给第5个小白鼠喝。
从编号倒数第6位是1的所有的瓶子里面取出1滴混在一起并标上记号为6.给第6个小白鼠喝。
以此类推。。。。
24小时过去了：
如果只有第一个小白鼠死了。即00000 00001，则第1瓶有毒。
如果只有第1,4个小白鼠死了。即00000 01001，则第9瓶有毒。

```

#### @@@剔除k个数求最长连续区间

最大剔除K个数，使得连续相同数最长。

输入

6  1

1 2 3 2 2 3

输出

3（k=1，移除3，则3个2可以连在一起，长度最长）

```java

class Solution {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
//        int n = scanner.nextInt();
//        int k = scanner.nextInt();
//        int[] nums = new int[n];
//        int maxLen = 0;
//        for (int i = 0; i < k; i++) {
//            nums[i] = scanner.nextInt();
//        }
//
        int k = 2;
        int[] nums = {1, 2, 3, 3, 2, 2, 2, 2, 3};
        int n = nums.length;
        int maxLen = 0;
        maxLen = func(nums, n, k);
        System.out.println(maxLen);
    }

    private static int func(int[] nums, int n, int k) {
        ArrayList[] visited = new ArrayList[10];
        int maxLen = 0;
        for (int i = 0; i < n; i++) {
            if (visited[nums[i]] != null) {
                visited[nums[i]].add(i);
            } else {
                ArrayList<Integer> list = new ArrayList<>();
                list.add(i);
                visited[nums[i]] = list;
            }
        }
        for (int i = 0; i < 10; i++) {
            ArrayList<Integer> temp = visited[i];
            ArrayList<Integer> distance = new ArrayList<>();
            if (temp == null) {
                continue;
            }
            for (int j = 1; j < temp.size(); j++) {
                distance.add(temp.get(j) - temp.get(j - 1) - 1);
            }
            int tempLen = getMaxLen(distance, k) + 1;
            maxLen = Math.max(tempLen, maxLen);
        }
        return maxLen;
    }

    private static int getMaxLen(ArrayList<Integer> distance, int k) {
        int ret = 0;
        int total = 0;
        int left = 0;
        for (int i = 0; i < distance.size(); i++) {
            total += distance.get(i);
            while (total > k) {
                total -= distance.get(left);
                left++;
            }
            ret = Math.max(ret, i - left + 1);
        }
        return ret;
    }
}
```

#### [LeetCode-624：Maximum Distance in Arrays (多数组找元素最大距离)](https://blog.csdn.net/Koala_Tree/article/details/78317657)

Question
Given m arrays, and each array is sorted in ascending order. Now you can pick up two integers from two different arrays (each array picks one) and calculate the distance. We define the distance between two integers a and b to be their absolute difference |a-b|. Your task is to find the maximum distance.

给定 m 个数组， 每个数组都已经按照升序排好序了。现在你需要从两个不同的数组中选择两个整数 （每个数组选一个） 并且计算它们的距离。 两个整数 a 和 b 之间的距离定义为它们差的绝对值 |a-b| 。 你的任务就是去找到最大距离

Example 1:

Input:
[[1,2,3],
 [4,5],
 [1,2,3]]

Output: 4

Explanation:
One way to reach the maximum distance 4 is to pick 1 in the first or third array and pick 5 in the second array.

Note:

+ Each given array will have at least 1 number. There will be at least two non-empty arrays；
+ The total number of the integers in all the m arrays will be in the range of [2, 10000]；
+ The integers in the m arrays will be in the range of [-10000, 10000].

```java
思路:
给定的多个（至少两个，且非空）按升序排序好的数组，从其中任意两个数组中各选择一个元素，计算两个元素的距离，找到最大距离。

class Solution {
    public int maxDistance(List<List<Integer>> arrays) {
        int res = 0;
        int min = arrays.get(0).get(0);
        int max = arrays.get(0).get(arrays.get(0).size() - 1);
        for (int i = 1; i < arrays.size(); i++) {
            List<Integer> array = arrays.get(i);
            res = Math.max(Math.abs(min - array.get(array.size() - 1)), Math.max(Math.abs(array.get(0) - max), res));
            min = Math.min(min, array.get(0));
            max = Math.max(max, array.get(array.size() - 1));
        }
        return res;
    }
}

```

#### @@@[移除子串](https://www.jianshu.com/p/067f2f150a49)

描述

> 给出一个字符串 s 以及 n 个子串。你可以从字符串 s 中移除 n 个子串中的任意一个，使得剩下来 s 的长度最小，输出这个最小长度。

样例

> 给出 s = ccdaabcdbb，子串为 ["ab","cd"]
>  返回 2.

解释

> ccdaabcdbb -> ccdacdbb -> cabb -> cb (长度为 2)

```java
  public int minLength(String s, Set<String> dict) {
        Queue<String> queue = new LinkedList<String>();
        Set<String> visited = new HashSet<String>();
        int min = s.length();
        queue.add(s);
        visited.add(s);

        while (!queue.isEmpty()) {
            String string = queue.poll();

            for (String sub : dict) {
                int found = string.indexOf(sub);

                while (found != -1) {
                    String tempStr = string.substring(0, found) + string.substring(found + sub.length(), string.length());
                    if (!visited.contains(tempStr)) {
                        if (tempStr.length() < min) {
                            min = tempStr.length();
                        }
                        queue.add(tempStr);
                        visited.add(tempStr);
                    }
                    found = string.indexOf(sub, found + 1);
                }

            }
        }
        return min;
    }
```

#### Java跳出多层循环

```java

public static void main(String[] args) {
        labelA:
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                System.out.print(i + "" + j + " ");
                if (i == 3 && j == 8) {
                    break labelA;
                }
            }
            System.out.println();
        }
    }
```

<file_sep>/notes/Java-集合.md

- [一、概览](#%e4%b8%80%e6%a6%82%e8%a7%88)
  - [Collection](#collection)
    - [1. Set](#1-set)
    - [2. List](#2-list)
    - [3. Queue](#3-queue)
  - [Map](#map)
- [二、容器中的设计模式](#%e4%ba%8c%e5%ae%b9%e5%99%a8%e4%b8%ad%e7%9a%84%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f)
  - [迭代器模式](#%e8%bf%ad%e4%bb%a3%e5%99%a8%e6%a8%a1%e5%bc%8f)
  - [适配器模式](#%e9%80%82%e9%85%8d%e5%99%a8%e6%a8%a1%e5%bc%8f)
- [三、源码分析](#%e4%b8%89%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%90)
  - [ArrayList](#arraylist)
    - [1. 概览](#1-%e6%a6%82%e8%a7%88)
    - [2. 扩容](#2-%e6%89%a9%e5%ae%b9)
    - [3. 删除元素](#3-%e5%88%a0%e9%99%a4%e5%85%83%e7%b4%a0)
    - [4. Fail-Fast](#4-fail-fast)
    - [5. 序列化](#5-%e5%ba%8f%e5%88%97%e5%8c%96)
  - [Vector](#vector)
    - [1. 同步](#1-%e5%90%8c%e6%ad%a5)
    - [2. 与 ArrayList 的比较](#2-%e4%b8%8e-arraylist-%e7%9a%84%e6%af%94%e8%be%83)
    - [3. 替代方案](#3-%e6%9b%bf%e4%bb%a3%e6%96%b9%e6%a1%88)
  - [CopyOnWriteArrayList](#copyonwritearraylist)
    - [读写分离](#%e8%af%bb%e5%86%99%e5%88%86%e7%a6%bb)
    - [适用场景](#%e9%80%82%e7%94%a8%e5%9c%ba%e6%99%af)
  - [LinkedList](#linkedlist)
    - [1. 概览](#1-%e6%a6%82%e8%a7%88-1)
    - [2. 与 ArrayList 的比较](#2-%e4%b8%8e-arraylist-%e7%9a%84%e6%af%94%e8%be%83-1)
  - [HashMap](#hashmap)
    - [1. 存储结构](#1-%e5%ad%98%e5%82%a8%e7%bb%93%e6%9e%84)
    - [2. 拉链法的工作原理](#2-%e6%8b%89%e9%93%be%e6%b3%95%e7%9a%84%e5%b7%a5%e4%bd%9c%e5%8e%9f%e7%90%86)
    - [3. put 操作](#3-put-%e6%93%8d%e4%bd%9c)
    - [4. 确定桶下标](#4-%e7%a1%ae%e5%ae%9a%e6%a1%b6%e4%b8%8b%e6%a0%87)
    - [5. 扩容-基本原理](#5-%e6%89%a9%e5%ae%b9-%e5%9f%ba%e6%9c%ac%e5%8e%9f%e7%90%86)
    - [6. 扩容-重新计算桶下标](#6-%e6%89%a9%e5%ae%b9-%e9%87%8d%e6%96%b0%e8%ae%a1%e7%ae%97%e6%a1%b6%e4%b8%8b%e6%a0%87)
    - [7. 计算数组容量](#7-%e8%ae%a1%e7%ae%97%e6%95%b0%e7%bb%84%e5%ae%b9%e9%87%8f)
    - [8. 链表转红黑树](#8-%e9%93%be%e8%a1%a8%e8%bd%ac%e7%ba%a2%e9%bb%91%e6%a0%91)
    - [9. 与 HashTable 的比较](#9-%e4%b8%8e-hashtable-%e7%9a%84%e6%af%94%e8%be%83)
    - [hashmap不支持并发的原因](#hashmap%e4%b8%8d%e6%94%af%e6%8c%81%e5%b9%b6%e5%8f%91%e7%9a%84%e5%8e%9f%e5%9b%a0)
  - [解决hash 冲突的三种方法](#%e8%a7%a3%e5%86%b3hash-%e5%86%b2%e7%aa%81%e7%9a%84%e4%b8%89%e7%a7%8d%e6%96%b9%e6%b3%95)
  - [ConcurrentHashMap](#concurrenthashmap)
    - [1. 存储结构](#1-%e5%ad%98%e5%82%a8%e7%bb%93%e6%9e%84-1)
    - [hashentry](#hashentry)
    - [2. size 操作](#2-size-%e6%93%8d%e4%bd%9c)
    - [3. JDK 1.8 的改动](#3-jdk-18-%e7%9a%84%e6%94%b9%e5%8a%a8)
  - [LinkedHashMap](#linkedhashmap)
    - [存储结构](#%e5%ad%98%e5%82%a8%e7%bb%93%e6%9e%84)
    - [afterNodeAccess()](#afternodeaccess)
    - [afterNodeInsertion() (保证缓存空间大小合适)](#afternodeinsertion-%e4%bf%9d%e8%af%81%e7%bc%93%e5%ad%98%e7%a9%ba%e9%97%b4%e5%a4%a7%e5%b0%8f%e5%90%88%e9%80%82)
    - [LRU 缓存](#lru-%e7%bc%93%e5%ad%98)
  - [WeakHashMap](#weakhashmap)
    - [存储结构](#%e5%ad%98%e5%82%a8%e7%bb%93%e6%9e%84-1)
    - [ConcurrentCache](#concurrentcache)

# 一、概览

容器主要包括 Collection 和 Map 两种，Collection 存储着对象的集合，而 Map 存储着键值对（两个对象）的映射表。

## Collection

### 1. Set

- TreeSet：基于红黑树实现，支持有序性操作，例如根据一个范围查找元素的操作。但是查找效率不如 HashSet，HashSet 查找的时间复杂度为 O(1)，TreeSet 则为 O(logN)。

- HashSet：基于哈希表实现，支持快速查找，但不支持有序性操作。并且失去了元素的插入顺序信息，也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。

- LinkedHashSet：具有 HashSet 的查找效率，且内部使用双向链表维护元素的插入顺序。

### 2. List

**ArrayList：基于动态数组实现，支持随机访问。**

ArrayList 通过数组实现，一旦我们实例化 ArrayList 无参数构造函数默认为数组初始化长度为 10
如果增加的元素个数超过了 10 个，那么 ArrayList 底层会新生成一个数组，长度为原数组的 1.5 倍+1，然后将原数组的内容复制到新数组当中，并且后续增加的内容都会放到新数组当中。

当新数组无法容纳增加的元素时，重复该过程。一旦数组超出长度，就开始扩容数组。扩容数组调用的方法 Arrays.copyOf(objArr, objArr.length + 1);

动态数组的本质就是，当数据超出当前数组的内存范围，这个时候，重新开辟一块大小为当前容量两倍的数组，把原数据拷贝过去，释放掉旧的数组， 这个时候内存有很多是没有使用的，这个时候就可以执行增删操作。

**Vector(已过时)：和 ArrayList 类似，但它是线程安全的。**

**LinkedList：基于双向链表实现，只能顺序访问，但是可以快速地在链表中间插入和删除元素。**

不仅如此，LinkedList 还可以用作栈、队列和双向队列。LinkedList 底层的数据结构是基于双向循环链表的，且头结点中不存放数据.

LinkedList类实现了Queue接口，因此我们可以把LinkedList当成Queue来用。

### 3. Queue

- LinkedList：可以用它来实现双向队列。LinkedList类实现了Queue接口，因此我们可以把LinkedList当成Queue来用。

- PriorityQueue：基于堆结构实现，可以用它来实现优先队列和大顶堆小顶堆(见LeetCode的topK问题).PriorityQueue的逻辑结构是一棵完全二叉树，存储结构其实是一个数组。逻辑结构层次遍历的结果刚好是一个数组。

offer，add 区别：

一些队列有大小限制，因此如果想在一个满的队列中加入一个新项，多出的项就会被拒绝。

这时新的 offer 方法就可以起作用了。它不是对调用 add() 方法抛出一个 unchecked 异常，而只是得到由 offer() 返回的 false。

poll，remove 区别：

remove() 和 poll() 方法都是从队列中删除第一个元素。remove() 的行为与 Collection 接口的版本相似， 但是新的 poll() 方法在用空集合调用时不是抛出异常，只是返回 null。因此新的方法更适合容易出现异常条件的情况。

peek，element区别：

element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似，在队列为空时， element() 抛出一个异常，而 peek() 返回 null。

## Map

- TreeMap：基于红黑树实现。

- HashMap：基于哈希表实现。

- HashTable：和 HashMap 类似，但它是线程安全的，这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类，不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全，并且 ConcurrentHashMap 的效率会更高，因为 ConcurrentHashMap 引入了分段锁。

- LinkedHashMap：使用双向链表来维护元素的顺序，顺序为插入顺序或者最近最少使用（LRU）顺序。

- ```java
    public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
        }
    ```

# 二、容器中的设计模式

## 迭代器模式

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);
}
```

## 适配器模式

java.util.Arrays#asList() 可以把数组类型转换为 List 类型。

```java
@SafeVarargs
public static <T> List<T> asList(T... a)
```

应该注意的是 asList() 的参数为泛型的变长参数，不能使用基本类型数组作为参数，只能使用相应的包装类型数组。

```java
Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
```

也可以使用以下方式调用 asList()：

```java
List list = Arrays.asList(1, 2, 3);
```

# 三、源码分析

如果没有特别说明，以下源码分析基于 JDK 1.8。

在 IDEA 中 double shift 调出 Search EveryWhere，查找源码文件，找到之后就可以阅读源码。

## ArrayList

### 1. 概览

因为 ArrayList 是基于数组实现的，所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。

```java
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
```

数组的默认大小为 10。

```java
private static final int DEFAULT_CAPACITY = 10;
```

### 2. 扩容

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够，如果不够时，需要使用 grow() 方法进行扩容，新容量的大小为 `oldCapacity + (oldCapacity >> 1)`，也就是旧容量的 1.5 倍。

扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中，这个操作代价很高，因此最好在创建 ArrayList 对象时就指定大概的容量大小，减少扩容操作的次数。

```java
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

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);
}
```

### 3. 删除元素

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上，该操作的时间复杂度为 O(N)，可以看出 ArrayList 删除元素的代价是非常高的。

```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;
}
```

### 4. 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();
    }
}
```

### 5. 序列化

ArrayList 基于数组实现，并且具有动态扩容特性，因此保存元素的数组不一定都会被使用，那么就没必要全部进行序列化。

保存元素的数组 elementData 使用 transient 修饰，该关键字声明数组默认不会被序列化。

```java
transient Object[] elementData; // non-private to simplify nested class access
```

ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。

```java
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}
```

```java
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
```

序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法，原理类似。

```java
ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
```

## Vector

### 1. 同步

它的实现与 ArrayList 类似，但是使用了 synchronized 进行同步。

```java
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}
```

### 2. 与 ArrayList 的比较

- Vector 是同步的，因此开销就比 ArrayList 要大，访问速度更慢。最好使用 ArrayList 而不是 Vector，因为同步操作完全可以由程序员自己来控制；
- Vector 每次扩容请求其大小的 2 倍空间，而 ArrayList 是 1.5 倍。

### 3. 替代方案

可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList。

```java
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
```

也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

```java
List<String> list = new CopyOnWriteArrayList<>();
```

## CopyOnWriteArrayList

### 读写分离

写操作在一个复制的数组上进行，读操作还是在原始数组中进行，读写分离，互不影响。

写操作需要加锁，防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

```java
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}
```

```java
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}
```

### 适用场景

CopyOnWriteArrayList 在写操作的同时允许读操作，大大提高了读操作的性能，因此很适合读多写少的应用场景。

但是 CopyOnWriteArrayList 有其缺陷：

- 内存占用：在写操作时需要复制一个新的数组，使得内存占用为原来的两倍左右；
- 数据不一致：读操作不能读取实时性的数据，因为部分写操作的数据还未同步到读数组中。

所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

## LinkedList

### 1. 概览

基于双向链表实现，使用 Node 存储链表节点信息。

```java
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}
```

每个链表存储了 first 和 last 指针：

```java
transient Node<E> first;
transient Node<E> last;
```

### 2. 与 ArrayList 的比较

ArrayList是实现了基于动态数组的结构，而LinkedList则是基于实现链表的数据结构。而两种数据结构在程序上体现出来的优缺点在于增删和改查的速率;

- ArrayList 基于动态数组实现，LinkedList 基于双向链表实现；
- ArrayList 支持随机访问，LinkedList 不支持；
- LinkedList 在任意位置添加删除元素更快。

在数据的更新和查找方面,因为arraylist的数据在同一个地址上,所以可以直接定位数据,查找和更新很快;LinkedList的每一个数据都有一个不同的地址,需要顺序查询然后更新,速度会慢很多;

在增加和删除上面,arraylist需要移动其他数据,LinkList只需要修改对应数据处的指向,所以LinkList快很多;

## HashMap

为了便于理解，以下源码分析以 JDK 1.7 为主。

### 1. 存储结构

内部包含了一个 Entry 类型的数组 table。

```java
transient Entry[] table;
```

Entry 存储着键值对。它包含了四个字段，从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶，一个桶存放一个链表。HashMap 使用拉链法来解决冲突，同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。

```java
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }
}
```

### 2. 拉链法的工作原理

```java
HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");
```

- 新建一个 HashMap，默认大小为 16；
- 插入 <K1,V1> 键值对，先计算 K1 的 hashCode 为 115，使用除留余数法得到所在的桶下标 115%16=3。
- 插入 <K2,V2> 键值对，先计算 K2 的 hashCode 为 118，使用除留余数法得到所在的桶下标 118%16=6。
- 插入 <K3,V3> 键值对，先计算 K3 的 hashCode 为 118，使用除留余数法得到所在的桶下标 118%16=6，插在 <K2,V2> 前面。

应该注意到链表的插入是以头插法方式进行的，例如上面的 <K3,V3> 不是插在 <K2,V2> 后面，而是插入在链表头部。

查找需要分成两步进行：

- 计算键值对所在的桶；
- 在链表上顺序查找，时间复杂度显然和链表的长度成正比。

### 3. put 操作

```java
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 键为 null 单独处理
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    // 确定桶下标
    int i = indexFor(hash, table.length);
    // 先找出是否已经存在键为 key 的键值对，如果存在的话就更新这个键值对的值为 value
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    // 插入新键值对
    addEntry(hash, key, value, i);
    return null;
    //put的返回值类型是V,本来以为会是插入成功的newValue,
    //但看了源码之后才发现,如果key存在则替换oldValue并返回oldValue,
    //如果不是更新而是插入新的entry则返回null.
}
```

HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法，也就无法确定该键值对的桶下标，只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。

```java
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}
```

使用链表的头插法，也就是新的键值对插在链表的头部，而不是链表的尾部。

```java
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    // 头插法，链表头部指向新的键值对
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
```

```java
Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}
```

### 4. 确定桶下标

很多操作都需要先确定一个键值对所在的桶下标。

```java
int hash = hash(key);
int i = indexFor(hash, table.length);
```

**4.1 计算 hash 值**

```java
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
```

```java
public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}
```

**4.2 取模**

令 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 取模效果是一样的：

```
y   : 10110010
x   : 00010000
y%x : 00000010
```

我们知道，位运算的代价比求模运算小的多，因此在进行这种计算时用位运算的话能带来更高的性能。

确定桶下标的最后一步是将 key 的 hash 值对桶个数取模：hash%capacity，如果能保证 capacity 为 2 的 n 次方，那么就可以将这个操作转换为位运算。

```java
static int indexFor(int h, int length) {
    return h & (length-1);
}
```

### 扩容长度是2n次方原因

2^n-1  为11111

key的hash  跟111想与不会丢失位数,如果跟110想与,则末尾永远是0

 在计算插入元素在hash桶数组的索引时第三步，为了使元素分布的更加均匀，用取模操作，但是传统取模操作效率低，然后优化成h&(length-1)，设置成2幂次方，是因为2的幂次方-1后的值每一位上都是1，然后与第二步计算出的h值与的时候，最终的结果只和key的hashcode值本身有关，这样不会造成空间浪费并且分布均匀，如果不是2的幂次方
 如果length不为2的幂，比如15。那么length-1的2进制就会变成1110。在h为随机数的情况下，和1110做&操作。尾数永远为0。那么0001、1001、1101等尾数为1的位置就永远不可能被entry占用。这样会造成浪费，不随机等问题。

### 5. 扩容-基本原理

设 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 的 n 次方。|
| size | 键值对数量。 |
| threshold | size 的临界值，当 size 大于等于 threshold 就必须进行扩容操作。 |
| loadFactor | 装载因子，table 能够使用的比例，threshold = capacity * loadFactor。|

```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 为原来的两倍,capacity必须是2的n次方。

```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() 实现，需要注意的是，扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中，因此这一步是很费时的。

```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);
        }
    }
}
```

### 6. 扩容-重新计算桶下标

在进行扩容时，需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制，可以降低重新计算桶下标的操作。

假设原数组长度 capacity 为 16，扩容之后 new capacity 为 32：

```html
capacity     : 00010000
new capacity : 00100000
```

对于一个 Key，

- 它的哈希值如果在第 5 位上为 0，那么取模得到的结果和之前一样；
- 如果为 1，那么得到的结果为原来的结果 +16。因为假设原来是17对16取模变成了1,如果现在是对32取模,17=16+1;

### 7. 计算数组容量

HashMap 构造函数允许用户传入的容量不是 2 的 n 次方，因为它可以自动地将传入的容量转换为 2 的 n 次方。

先考虑如何求一个数的掩码，对于 10010000，它的掩码为 11111111，可以使用以下方法得到：

```
mask |= mask >> 1    11011000
mask |= mask >> 2    11111110
mask |= mask >> 4    11111111
```

mask+1 是大于原始数字的最小的 2 的 n 次方。

```
num     10010000
mask+1 100000000
```

以下是 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;
}
```

### 8. 链表转红黑树

从 JDK 1.8 开始，一个桶存储的链表长度大于 8 时会将链表转换为红黑树。

### 9. 与 HashTable 的比较(不支持插入null Entry)

- HashTable 使用 synchronized 来进行同步。
- HashMap 可以插入键为 null 的 Entry。
- HashMap 的迭代器是 fail-fast 迭代器。
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。

### hashmap不支持并发的原因

1. #### 向HashMap中插入数据的时候

　　在HashMap做put操作的时候会调用到以下的方法：

//向HashMap中添加Entry

```
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length); //扩容2倍
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);

}
//创建一个Entry
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];//先把table中该位置原来的Entry保存
    //在table中该位置新建一个Entry，将原来的Entry挂到该Entry的next
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    //所以table中的每个位置永远只保存一个最新加进来的Entry，其他Entry是一个挂一个，这样挂上去的
    size++;
}
```

　　现在假如A线程和B线程同时进入addEntry，然后计算出了相同的哈希值对应了相同的数组位置，因为此时该位置还没数据，然后对同一个数组位置调用createEntry，两个线程会同时得到现在的头结点，然后A写入新的头结点之后，B也写入新的头结点，那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。

　　现在假如A线程和B线程同时进入addEntry，然后计算出了相同的哈希值对应了相同的数组位置，因为此时该位置还没数据，然后对同一个数组位置调用createEntry，两个线程会同时得到现在的头结点，然后A写入新的头结点之后，B也写入新的头结点，那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。

　　现在假如A线程和B线程同时进入addEntry，然后计算出了相同的哈希值对应了相同的数组位置，因为此时该位置还没数据，然后对同一个数组位置调用createEntry，两个线程会同时得到现在的头结点，然后A写入新的头结点之后，B也写入新的头结点，那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。

2. #### HashMap扩容的时候

　　还是上面那个addEntry方法中，有个扩容的操作，这个操作会新生成一个新的容量的数组，然后对原数组的所有键值对重新进行计算和写入新的数组，之后指向新生成的数组。来看一下扩容的源码：

//用新的容量来给table扩容

```
void resize(int newCapacity) {
    Entry[] oldTable = table; //保存old table
    int oldCapacity = oldTable.length; //保存old capacity
    // 如果旧的容量已经是系统默认最大容量了，那么将阈值设置成整形的最大值，退出
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    //根据新的容量新建一个table
    Entry[] newTable = new Entry[newCapacity];
    //将table转换成newTable
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    //设置阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

}
```

　　那么问题来了，当多个线程同时进来，检测到总数量超过门限值的时候就会同时调用resize操作，各自生成新的数组并rehash后赋给该map底层的数组table，结果最终只有最后一个线程生成的新数组被赋给table变量，其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候，就会用已经被赋值的table作为原始数组，这样也会有问题。所以在扩容操作的时候也有可能会引起一些并发的问题。

#### get和resize导致死循环

另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环（cpu100%），具体分析如下：

下面的代码是resize的核心内容：

```java
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {

            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
```

这个方法的功能是将原来的记录重新计算在新桶的位置，然后迁移过去。

![image-20200303160356687](pictures/Java-集合/image-20200303160356687.png)

多线程HashMap的resize

我们假设有两个线程同时需要执行resize操作，我们原来的桶数量为2，记录数为3，需要resize桶到4，原来的记录分别为：[3,A],[7,B],[5,C]，在原来的map里面，我们发现这三个entry都落到了第二个桶里面。
 假设线程thread1执行到了transfer方法的Entry next = e.next这一句，然后时间片用完了，此时的e = [3,A], next = [7,B]。线程thread2被调度执行并且顺利完成了resize操作，需要注意的是，此时的[7,B]的next为[3,A]。此时线程thread1重新被调度运行，此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,A]迁移到新的数组上，然后再处理[7,B]，而[7,B]被链接到了[3,A]的后面，处理完[7,B]之后，就需要处理[7,B]的next了啊，而通过thread2的resize之后，[7,B]的next变为了[3,A]，此时，[3,A]和[7,B]形成了环形链表，在get的时候，如果get的key的桶索引和[3,A]和[7,B]一样，那么就会陷入死循环。

如果在取链表的时候从头开始取（现在是从尾部开始取）的话，则可以保证节点之间的顺序，那样就不存在这样的问题了。

## 如何线程安全的使用map

- 将所有public方法都加上synchronized. 相当于设置了一把全局锁，所有操作都需要先获取锁，java.util.HashTable就是这么做的，性能很低
- **由于每个桶在逻辑上是相互独立的，将每个桶都加一把锁，如果两个线程各自访问不同的桶，就不需要争抢同一把锁了。这个方案的并发性比单个全局锁的性能要好，不过锁的个数太多，也有很大的开销。**
- **锁分离(Lock Stripping)技术。第2个方法把锁的压力分散到了多个桶，理论上是可行的的，但是假设有1万个桶，就要新建1万个ReentrantLock实例，开销很大。可以将所有的桶均匀的划分为16个部分，每一部分成为一个段(Segment)，每个段上有一把锁，这样锁的数量就降低到了16个。JDK 7里的java.util.concurrent.ConcurrentHashMap就是这个思路。**
- 在JDK8里，ConcurrentHashMap的实现又有了很大变化，它在锁分离的基础上，大量利用了了CAS指令。并且底层存储有一个小优化，当链表长度太长（默认超过8）时，链表就转换为红黑树。链表太长时，增删查改的效率比较低，改为红黑树可以提高性能。JDK8里的ConcurrentHashMap有6000多行代码，JDK7才1500多行。
-

## 红黑树的介绍

### 二叉查找树

二叉查找树，也称有序二叉树（ordered binary tree），或已排序二叉树（sorted binary tree），是指一棵空树或者具有下列性质的二叉树：

若任意节点的左子树不空，则左子树上所有结点的值均小于它的根结点的值；
若任意节点的右子树不空，则右子树上所有结点的值均大于它的根结点的值；
任意节点的左、右子树也分别为二叉查找树。
没有键值相等的节点（no duplicate nodes）。

因为一棵由n个结点随机构造的二叉查找树的高度为lgn，所以顺理成章，二叉查找树的一般操作的执行时间为O(lgn)。**但二叉查找树若退化成了一棵具有n个结点的线性链后，则这些操作最坏情况运行时间为O(n)。**

### 平衡二叉查找树

*AVL树*是根据它的发明者G.M.Adelson-Velsky和E.M.Landis命名的。

左右子树层数差别不超过1

### 红黑树

 红黑树虽然本质上是一棵二叉查找树，但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡，从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。

但它是如何保证一棵n个结点的红黑树的高度始终保持在logn的呢？这就引出了红黑树的5个性质：

1. 每个结点要么是红的要么是黑的。
2.  根结点是黑的。
3. 每个叶结点（叶结点即指树尾端NIL指针或NULL结点）都是黑的。
4. 一个结点是红的，那么它的两个儿子都是黑的。
5.  对于任意结点而言，其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。

**正是红黑树的这5条性质，使一棵n个结点的红黑树始终保持了logn的高度，从而也就解释了上面所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)**”这一结论成立的原因。

### 红黑树比AVL好的地方

红黑树的**查询性能略微逊色于AVL树**，因为他比avl树会稍微不平衡最多一层，也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较，

但是，红黑树在插**入和删除上完爆avl树**，avl树每次插入删除会进行大量的平衡度计算，而红黑树为了维持红黑性质所做的红黑变换和旋转的开销，相较于avl树为了维持平衡的开销要小得多

## 解决hash 冲突的三种方法
1. 拉链法

HashSet其实都是采用的拉链法来解决哈希冲突的，就是在每个位桶实现的时候，我们采用链表（jdk1.8之后采用链表+红黑树）的数据结构来去存取发生哈希冲突的输入域的关键字（也就是被哈希函数映射到同一个位桶上的关键字）。

2. 开放地址法

开放地址法有个非常关键的特征，就是所有输入的元素全部存放在哈希表里，也就是说，位桶的实现是不需要任何的链表来实现的，换句话说，也就是这个哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候，先通过哈希函数进行判断，若是发生哈希冲突，就以当前地址为基准，根据再寻址的方法（探查序列），去寻找下一个地址，若发生冲突再去寻找，直至找到一个为空的地址为止。所以这种方法又称为再散列法。

3. 重新哈希

再散列法其实很简单，就是再使用哈希函数去散列一个输入的时候，输出是同一个位置就再次散列，直至不发生冲突位置

## ConcurrentHashMap

### 1. 存储结构

```java
static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}
```

ConcurrentHashMap 和 HashMap 实现上类似，最主要的差别是 ConcurrentHashMap 采用了分段锁（Segment），每个分段锁维护着几个桶（HashEntry），多个线程可以同时访问不同分段锁上的桶，从而使其并发度更高（并发度就是 Segment 的个数）。

Segment 继承自 ReentrantLock,使得每一个segment可以充当锁的角色

```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;
```

### hashentry
图中淡蓝色的部分就是两个hashentry,在HashEntry类中，

**key，hash和next域都被声明为final的，**

**value域被volatile所修饰(所以其可以确保被读线程读到最新的值)，因此HashEntry对象几乎是不可变的，这是ConcurrentHashmap读操作并不需要加锁的重要原因。**

 next域被声明为final本身就意味着我们不能从hash链的中间或尾部添加或删除节点，因为这需要修改next引用值，因此所有的节点的修改只能从头部开始。**remove的时候需要全部重新new一遍**

### 2. size 操作
size操作用来返回map的键值对个数

**每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。维护一个modCount变量来统计每个segment内部的操作次数,这个值只增不减.**

**size操作就是遍历了两次所有的Segments，每次记录Segment的modCount值，然后将两次的modCount进行比较，如果modCount相同，则表示期间没有发生过写入操作，就将原先遍历的count的求和结果返回，如果modCount求和不相同，则把这个过程再重复做一次;如果modCount求和再不相同，则就需要将所有的Segment都锁住，然后一个一个遍历了,求出segment的求和并返回.**

```java
/**
 * The number of elements. Accessed only either within locks
 * or among other volatile reads that maintain visibility.
 */
transient int count;
```

在执行 size 操作时，需要遍历所有 Segment 然后把 count 累计起来。

ConcurrentHashMap 在执行 size 操作时先尝试不加锁，如果连续两次不加锁操作得到的结果一致，那么可以认为这个结果是正确的。

尝试次数使用 RETRIES_BEFORE_LOCK 定义，该值为 2，retries 初始值为 -1，因此尝试次数为 3。

如果尝试的次数超过 3 次，就需要对每个 Segment 加锁。

```java

/**
 * Number of unsynchronized retries in size and containsValue
 * methods before resorting to locking. This is used to avoid
 * unbounded retries if tables undergo continuous modification
 * which would make it impossible to obtain an accurate result.
 */
static final int RETRIES_BEFORE_LOCK = 2;

public int size() {
    // Try a few times to get accurate count. On failure due to
    // continuous async changes in table, resort to locking.
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
            // 超过尝试次数，则对每个 Segment 加锁
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            // 连续两次得到的结果一致，则认为这个结果是正确的
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}
```

### 3. JDK 1.8 的改动

JDK 1.7 使用分段锁机制来实现并发更新操作，核心类为 Segment，它继承自重入锁 ReentrantLock，并发度与 Segment 数量相等。

JDK 1.8 使用了 CAS 操作来支持更高的并发度，在 CAS 操作失败时使用内置锁 synchronized。

并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。

## LinkedHashMap

### 存储结构

继承自 HashMap，因此具有和 HashMap 一样的快速查找特性。

```java
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
```

通过每一个entry内部增加before和after,内部维护了一个双向链表，用来维护插入顺序或者 LRU(Least Recently Used) 顺序。

```java
新Entry的所有成员变量:

K key
V value
Entry<K, V> next
int hash
Entry<K, V> before
Entry<K, V> after

/**
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> tail;
```

accessOrder 决定了顺序，默认为 false，此时维护的是插入顺序。为true的时候,将会维护最近访问顺序(get的entry放在链表末尾)

```java
final boolean accessOrder;
```

LinkedHashMap 最重要的是以下用于维护顺序的函数，它们会在 put、get 等方法中调用。

```java
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
```

### afterNodeAccess()

当一个节点被访问时，如果 accessOrder 为 true，则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后，在每次访问一个节点时，会将这个节点移到链表尾部，保证链表尾部是最近访问的节点，那么链表首部就是最近最久未使用的节点。

```java
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
```

### afterNodeInsertion()  (保证缓存空间大小合适)

在 put 等操作之后执行，当 removeEldestEntry() 方法返回 true 时会移除最晚的节点，也就是链表首部节点 first。

evict 只有在构建 Map 的时候才为 false，在这里为 true。

```java
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}
```

removeEldestEntry() 默认为 false，如果需要让它为 true，需要继承 LinkedHashMap 并且覆盖这个方法的实现，这在实现 LRU 的缓存中特别有用，通过移除最近最久未使用的节点，从而保证缓存空间足够，并且缓存的数据都是热点数据。

```java
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
```

### LRU 缓存

以下是使用 LinkedHashMap 实现的一个 LRU 缓存：

- 设定最大缓存空间 MAX_ENTRIES  为 3；
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true，开启 LRU 顺序；
- 覆盖 removeEldestEntry() 方法实现，在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。

```java
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_ENTRIES = 3;

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }

    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}
```

```java
public static void main(String[] args) {
    LRUCache<Integer, String> cache = new LRUCache<>();
    cache.put(1, "a");
    cache.put(2, "b");
    cache.put(3, "c");
    cache.get(1);
    cache.put(4, "d");
    System.out.println(cache.keySet());
}
```

```html
[3, 1, 4]
```

## WeakHashMap

### 存储结构

WeakHashMap 的 Entry 继承自 WeakReference(弱引用)，被 WeakReference 关联的对象在下一次垃圾回收时会被回收。弱引用会在下次gc的时候被回收,软引用会在内存不够的时候回收.

WeakHashMap 主要用来实现缓存，通过使用 WeakHashMap 来引用缓存对象，由 JVM 对这部分缓存进行回收。

```java
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
```

### ConcurrentCache

Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。

ConcurrentCache 采取的是分代缓存：

- 经常使用的对象放入 eden 中，eden 使用 ConcurrentHashMap 实现，不用担心会被回收（伊甸园）；
- 不常用的对象放入 longterm，longterm 使用 WeakHashMap 实现，这些老对象会被垃圾收集器回收。
- 当调用  get() 方法时，会先从 eden 区获取，如果没有找到的话再到 longterm 获取，当从 longterm 获取到就把对象放入 eden 中，从而保证经常被访问的节点不容易被回收。
- 当调用 put() 方法时，如果 eden 的大小超过了 size，那么就将 eden 中的所有对象都放入 longterm 中，利用虚拟机回收掉一部分不经常使用的对象。

```java
public final class ConcurrentCache<K, V> {

    private final int size;

    private final Map<K, V> eden;

    private final Map<K, V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            v = this.longterm.get(k);
            if (v != null)
                this.eden.put(k, v);
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            //先把eden中的全部放入longterm
            this.longterm.putAll(this.eden);
            //然后清空eden
            this.eden.clear();
        }
        //然后在清空的eden中放入要插入的k,v
        this.eden.put(k, v);
    }
}
```

<file_sep>/notes/Java-基础.md

- [数据类型](#数据类型)
    - [基本数据类型](#基本数据类型)
    - [拆箱与装箱](#拆箱与装箱)
    - [常量池](#常量池)
        - [定义](#定义)
        - [好处](#好处)
    - [包装类与常量池](#包装类与常量池)
        - [定义](#定义-1)
        - [场景](#场景)
        - [拓展](#拓展)
- [String](#string)
    - [概览](#概览)
    - [不可变的好处](#不可变的好处)
    - [构造函数](#构造函数)
- [String,StringBuffer,StringBuilder的区别](#stringstringbufferstringbuilder的区别)
    - [StrinBuffer和StringBuilder的常用方法](#strinbuffer和stringbuilder的常用方法)
    - [String Pool](#string-pool)
    - [new String("abc")创建两个对象](#new-stringabc创建两个对象)
- [引用类型与传值传址](#引用类型与传值传址)
- [面向对象三大特性](#面向对象三大特性)
    - [继承](#继承)
    - [封装](#封装)
    - [多态](#多态)
        - [三种实现方式](#三种实现方式)
        - [表现形式](#表现形式)
        - [多态的实现原理](#多态的实现原理)
    - [接口](#接口)
    - [抽象类](#抽象类)
        - [定义](#定义-2)
        - [与普通类,接口的区别](#与普通类接口的区别)
        - [与接口的区别](#与接口的区别)
        - [java只有单继承,接口却可以多实现的原因](#java只有单继承接口却可以多实现的原因)
- [父子类构造器的调用顺序](#父子类构造器的调用顺序)
    - [默认调无参](#默认调无参)
    - [有参用super](#有参用super)
    - [基类private属性](#基类private属性)
    - [派生类的覆盖](#派生类的覆盖)
- [重写与重载](#重写与重载)
    - [重写(Override)](#重写override)
        - [定义](#定义-3)
        - [场景](#场景-1)
        - [调用父类被重写的方法](#调用父类被重写的方法)
    - [重写的向上向下转型](#重写的向上向下转型)
        - [向上转型(子类向上转成父类来用)](#向上转型子类向上转成父类来用)
        - [向下转型](#向下转型)
    - [静态分派和动态分派](#静态分派和动态分派)
    - [重载(Overload)](#重载overload)
        - [定义](#定义-4)
        - [重载规则:](#重载规则)
        - [重载和重写的区别](#重载和重写的区别)
- [Object 通用方法](#object-通用方法)
    - [概览](#概览-1)
    - [== 与 equal](#-与-equal)
    - [hashCode()](#hashcode)
    - [toString()](#tostring)
    - [clone()](#clone)
- [关键字](#关键字)
    - [final](#final)
        - [设计一个不可变的类](#设计一个不可变的类)
            - [不可变模式](#不可变模式)
    - [static](#static)
    - [native](#native)
        - [定义](#定义-5)
        - [JN的书写步骤](#jn的书写步骤)
- [反射](#反射)
    - [定义](#定义-6)
    - [获取Class对象](#获取class对象)
    - [创建实例](#创建实例)
    - [使用反射获取类信息](#使用反射获取类信息)
        - [使用反射获取变量信息](#使用反射获取变量信息)
        - [使用反射获取方法信息](#使用反射获取方法信息)
    - [访问和操作私有变量与方法](#访问和操作私有变量与方法)
        - [访问私有方法](#访问私有方法)
        - [修改私有变量](#修改私有变量)
        - [修改私有常量](#修改私有常量)
    - [应用场景](#应用场景)
- [异常](#异常)
    - [分类](#分类)
    - [异常处理机制](#异常处理机制)
        - [异常捕获语句 try-catch](#异常捕获语句-try-catch)
        - [异常捕获语句 try－catch-finally](#异常捕获语句-trycatch-finally)
    - [执行顺序](#执行顺序)
    - [异常链](#异常链)
    - [常见异常](#常见异常)
- [泛型](#泛型)
    - [定义](#定义-7)
    - [好处](#好处-1)
    - [泛型类](#泛型类)
    - [泛型接口](#泛型接口)
    - [泛型方法](#泛型方法)
    - [泛型通配符](#泛型通配符)
        - [通配符介绍](#通配符介绍)
        - [通配符的上下边界](#通配符的上下边界)
    - [泛型擦除](#泛型擦除)
        - [泛型擦除的优缺点](#泛型擦除的优缺点)
        - [java中没有泛型数组的原因](#java中没有泛型数组的原因)
- [注解](#注解)
    - [定义](#定义-8)
    - [原理](#原理)
    - [使用场景](#使用场景)
    - [常见元注解](#常见元注解)
- [内部变量](#内部变量)
- [内部类](#内部类)
    - [定义](#定义-9)
    - [特点](#特点)
    - [作用](#作用)
    - [内部类的分类](#内部类的分类)
        - [成员内部类](#成员内部类)
        - [局部内部类](#局部内部类)
        - [静态内部类](#静态内部类)
        - [匿名内部类](#匿名内部类)
        - [匿名内部类的形参要使用final](#匿名内部类的形参要使用final)
- [特性](#特性)
    - [Java 各版本的新特性](#java-各版本的新特性)
    - [Java 与 C++ 的区别](#java-与-c-的区别)
    - [JRE or JDK](#jre-or-jdk)
- [Java根类Object的方法](#java根类object的方法)
    - [getClass方法](#getclass方法)
    - [equals方法](#equals方法)
    - [hashCode方法](#hashcode方法)
    - [clone方法](#clone方法)
    - [toString方法](#tostring方法)
    - [notify方法](#notify方法)
    - [notifyAll方法](#notifyall方法)
    - [wait(long timeout) throws InterruptedException方法](#waitlong-timeout-throws-interruptedexception方法)
    - [finalize方法](#finalize方法)

# 数据类型

## 基本数据类型

short 16位
int 32位 4字节
long 64位

float 32位
double 64位

char 16位
byte 8位
boolean 1位

boolean 只有两个值：true、false，可以使用 1 bit 来存储，但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int，使用 1 来表示 true，0 表示 false。JVM 支持 boolean 数组，但是是通过读写 byte 数组来实现的。

例如byte类型是1byte也就是8位，可以表示的数字是-128到127，因为还有一个0，加起来一共是256，也就是2的八次方。

##  拆箱与装箱

基本数据类型与它的包装类之间的相互转换

在一个类里面,基本数据类型的属性尽量使用包装类.

```java
Integer x = 2;     // 装箱
int y = x;         // 拆箱
```

## 常量池
### 定义
常量池分为静态常量池和运行时常量池;
静态常量池是.class文件的常量池,包含字符串和类方法信息;
运行时常量池是jvm虚拟机在完成类装载操作后，将class文件中的常量池载入到内存中，并保存在**方法区**中，我们常说的常量池，就是指方法区中的运行时常量池。

### 好处
避免频繁的创建和销毁对象而影响系统性能，其实现了对象的共享。
例如字符串常量池，在编译阶段就把所有的字符串文字放到一个常量池中。
（1）节省内存空间：常量池中所有相同的字符串常量被合并，只占用一个空间。
（2）节省运行时间：比较字符串时，==比equals()快。对于两个引用变量，只用==判断引用是否相等，也就可以判断实际值是否相等。

## 包装类与常量池
### 定义
Byte,Short,Integer,Long,Character,Boolean的[-128,127]都实现了常量池技术,两种浮点类型float,double没有实现,因为在任何区间内都有无限个数值.

### 场景

new Integer(123) 与 Integer.valueOf(123) 的区别在于：

- new Integer(123) 每次都会新建一个对象；
- Integer.valueOf(123) 会使用缓存池中的对象，多次调用会取得同一个对象的引用。

```java
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true
```

valueOf() 方法的实现比较简单，就是先判断值是否在缓存池中，如果在的话就直接返回缓存池的内容。

```java
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
```
### 拓展

在 jdk 1.8 所有的数值类缓冲池中，Integer 的缓冲池 IntegerCache 很特殊，这个缓冲池的下界是 - 128，上界默认是 127，但是这个上界是可调的，在启动 jvm 的时候，通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小，该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性，然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。

# String

## 概览

**String 被声明为 final，因此它不可被继承。**

在 Java 8 中，String 内部使用 char 数组存储数据。

```java
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}
```

在 Java 9 之后，String 类的实现改用 byte 数组存储字符串，同时使用 `coder` 来标识使用了哪种编码。

```java
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}
```

value 数组被 1. 声明为 final，这意味着 value 数组初始化之后就不能再引用其它数组。2. 并且 String 内部没有改变 value 数组的方法，3. 而且为了防止value内部被修改,还是用了private来防止value的内容被修改,因此可以保证 String 不可变。

## 不可变的好处

**1. 可以缓存 hash 值**

因为 String 的 hash 值经常被使用，例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变，因此只需要进行一次计算。

**2. String Pool 的需要**

如果一个 String 对象已经被创建过了，那么就会从 String Pool 中取得引用。只有 String 是不可变的，才可能使用 String Pool。

**3. 安全性**

String 经常作为参数，String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的，那么在网络连接过程中，String 被改变，改变 String 对象的那一方以为现在连接的是其它主机，而实际情况却不一定是。

**4. 线程安全**

String 不可变性天生具备线程安全，可以在多个线程中安全地使用。

[Program Creek : Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/)

## 构造函数
以下是 String 构造函数的源码，可以看到，在将一个字符串对象作为另一个字符串对象的构造函数参数时，并不会完全复制 value 数组内容，而是都会指向同一个 value 数组。

```java
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
```

# String,StringBuffer,StringBuilder的区别

**1. 可变性**

- String 不可变,任何对String的改变都 会引发新的String对象的生成
- StringBuffer 和 StringBuilder 可变;任何对它所指代的字符串的改变都不会产生新的对象。

**2. 线程安全**

- String 不可变，因此是线程安全的
- StringBuilder 不是线程安全的,单线程效率更高
- StringBuffer 是线程安全的，内部使用 synchronized 进行同步

在字符串操作频繁的情况下,String的效率远低于其他两个
```
代码:StringTest.java
```

## StrinBuffer和StringBuilder的常用方法
都继承自AbstractStringBuilder,主要的存储是 chars[] value,和 capicity

StringBuffer s = new StringBuffer();
capicity=16

StringBuffer sb2=new StringBuffer(“how are you?”)
初始化的时候,capicity=传入字符串的长度+16 即为内部chars[] value的长度

扩容大小 尝试将新容量扩为大小变成2倍+2

## String Pool

字符串常量池（String Pool）保存着所有字符串字面量（literal strings），这些字面量在编译时期就确定。不仅如此，还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时，如果 String Pool 中已经存在一个字符串和该字符串值相等（使用 equals() 方法进行确定），那么就会返回 String Pool 中字符串的引用；否则，就会在 String Pool 中添加一个新的字符串，并返回这个新字符串的引用。

下面示例中，s1 和 s2 采用 new String() 的方式新建了两个不同字符串，而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中，然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。

```java
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4);           // true
```

如果是采用 "bbb" 这种字面量的形式创建字符串，会自动地将字符串放入 String Pool 中。

```java
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6);  // true
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false
```

在 Java 7 之前，String Pool 被放在运行时常量池中，它属于永久代。而在 Java 7，String Pool 被移到堆中。这是因为永久代的空间有限，在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning)
- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html)

## new String("abc")创建两个对象

使用这种方式一共会创建两个字符串对象（前提是 String Pool 中还没有 "abc" 字符串对象）。

- "abc" 属于字符串字面量，因此编译时期会在 String Pool 中创建一个字符串对象，指向这个 "abc" 字符串字面量；
- 而使用 new 的方式会在堆中创建一个字符串对象。

创建一个测试类，其 main 方法中使用这种方式来创建字符串对象。

```java
public class NewStringTest {
    public static void main(String[] args) {
        String s = new String("abc");
    }
}
```

使用 javap -verbose 进行反编译，得到以下内容：

```java
// ...
Constant pool:
// ...
   #2 = Class              #18            // java/lang/String
   #3 = String             #19            // abc
// ...
  #18 = Utf8               java/lang/String
  #19 = Utf8               abc
// ...

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class java/lang/String
         3: dup
         4: ldc           #3                  // String abc
         6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
// ...
```

在 Constant Pool 中，#19 存储这字符串字面量 "abc"，#3 是 String Pool 的字符串对象，它指向 #19 这个字符串字面量。在 main 方法中，0: 行使用 new #2 在堆中创建一个字符串对象，并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。

# 引用类型与传值传址
在java里面除基本数据类型的其它类型都是引用数据类型，自己定义的class类都是引用类型

基本数据类型在往方法传值的时候,传递的是值

引用类型在往方法传值的时候,传递的是引用地址(String类型,类类型都是引用类型)

# 面向对象三大特性

继承,封装,多态

## 继承
Java中的继承只能单继承，但是可以通过内部类继承其他类来实现多继承。
```java
    public class Son extends Father {
        public void go() {
            System.out.println("son go");
        }

        public void eat() {
            System.out.println("son eat");
        }

        public void sleep() {
            System.out.println("zzzzzz");
        }

        public void cook() {
//匿名内部类实现的多继承
            new Mother().cook();
//内部类继承第二个父类来实现多继承
            Mom mom = new Mom();
            mom.cook();
        }

        private class Mom extends Mother {
            @Override
            public void cook() {
                System.out.println("mom cook");
            }
        }
    }
```

## 封装
用访问修饰符来保证类内部的数据不会被外部随意访问修改

```java
private : 在同一类内可见。使用对象：变量、方法。 注意：不能修饰类（外部类）
default (即默认，什么也不写）: 在同一包内可见，不使用任何修饰符。使用对象：类、接口、变量、方法。
protected : 对同一包内的类和所有子类可见。使用对象：变量、方法。 注意：不能修饰类（外部类）。
public : 对所有类可见。使用对象：类、接口、变量、方法
```

## 多态
同一个方法或者类具有不同的功能或者表现形式

### 三种实现方式
1. 重写与重载

从jvm实现的角度来看，重写又叫运行时多态，编译时看不出子类调用的是哪个方法，但是运行时操作数栈会先根据子类的引用去子类的类信息中查找方法，找不到的话再到父类的类信息中查找方法。

而重载则是编译时多态，因为编译期就可以确定传入的参数组合，决定调用的具体方法是哪一个了。

2. 接口

3. 抽象类与抽象方法

### 表现形式
对象上 向上转型与向下转型

方法上 重写与重载
### 多态的实现原理

## 接口
接口是抽象类的延伸，在 Java 8 之前，它可以看成是一个完全抽象的类，也就是说它不能有任何的方法实现。

从 Java 8 开始，接口也可以拥有默认的方法实现，这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前，如果一个接口想要添加新的方法，那么要修改所有实现了该接口的类。

接口的成员（字段 + 方法）默认都是 public 的，并且不允许定义为 private 或者 protected。

接口的字段默认都是 static 和 final 的。

## 抽象类
### 定义
abstract class的核心在于，我知道一类物体的部分行为（和属性），但是不清楚另一部分的行为（和属性），所以我不能自己实例化。
### 与普通类,接口的区别
抽象类和普通类最大的区别是，抽象类不能被实例化，需要继承抽象类才能实例化其子类。

### 与接口的区别
- 抽象类与接口的区别是,抽象类更看重类之间的继承关系,父类某些方法要被子类继承,另外一些特定方法由子类具体实现;接口不是父子继承关系,而是满足某个标准所以要实现什么样的功能;抽象类是针对父子继承的抽象,接口是针对标准满足的抽象;
- 从使用上来看，一个类可以实现多个接口，但是不能继承多个抽象类。
- 接口的字段只能是 static 和 final 类型的，而抽象类的字段没有这种限制。
- 接口的成员只能是 public 的，而抽象类的成员可以有多种访问权限。

### java只有单继承,接口却可以多实现的原因

举个例子，在这里有个A类，我们又编写了两个类B类和C类，并且B类和C类分别继承了A类，并且对A类的同一个方法进行了覆盖。如果此时我们再次编写了一个D类，并且D类以多继承的方式同时集成了B类和C类，那么D类也会继承B类和C类从A类中重载的方法，那么问题来了，D类也开始犯迷糊了，我到底应该哪个继承哪个类中的方法呢，因为类是结构性的，这样就会造成结构上的混乱。这就是多继承的菱形继承问题。

因为接口只有抽象方法，具体方法只能由实现接口的类实现，在调用的时候始终只会调用实现类（也就是子类覆盖的方法）的方法（不存在歧义），因此即使继承的两个接口中的方法名是一样的，最终调用的时候也都是调用实现类中的那个方法，不会缠身歧义；而又因为接口只有静态的常量，但是由于静态变量是在编译期决定调用关系的，即使存在一定的冲突也会在编译时提示出错；而引用静态变量一般直接使用类名或接口名，从而避免产生歧义，因此从接口的变量是来看也是可以通过的。

# 父子类构造器的调用顺序
## 默认调无参
派生类的构造器会必须调用基类的构造器,如果不指定,则默认调用无参构造器(所以基类中推荐要有无参构造器)

```java
public class test {

    public static void main(String[] args) {
        new Student();
    }
}

class Humans {
    public Humans() {
        System.out.println("我是人");
    }
}

class Student extends Humans{
    public Student() {
        System.out.println("我是学生");
    }
}
```
输出为

```
我是人
我是学生
```
## 有参用super
构造器有参数时，那就必须使用关键字super现实地编写调用基类构造器的代码，并且匹配适当的参数列表。(要注意的是，super必须在构造器的最前面，不然会报错。)

```java

    public static void main(String[] args) {
        new Student("sx");
    }
}

class Humans {
    public Humans() {
        System.out.println("我是人");
    }

    public Humans(String name){
        System.out.println("我是叫"+name+"的人");
    }
}

class Student extends Humans{
    public Student() {
        System.out.println("我是学生");
    }

    Student(String name){
        super(name);
        System.out.println("我是叫"+name+"的学生!");
    }

```
输出结果为:

```
我是叫sx的人
我是叫sx的学生!
```
**如果注释掉上面的super(name);将会报错。原因是派生类必须调用基类构造器。因为实例化派生类时，基类也会被实例化，如果不调用基类的有参构造器，基类又没有可以给派生类使用的默认无参构造器,基类将不会被实例化，所以派生类没有调用基类构造器会报错。**

**原因是，如果基类有一个无参的构造器，就算派生类不用super显示调用基类的构造函数，编译器也会自动去调用基类的无参构造函数。**

## 基类private属性
**如果派生类使用了访问修饰符,则派生类要想访问基类里面被private和default修饰的属性,需要用getter和setter**

```java
public class Humans {

	public String sex;

	protected int age ;

	private String name;

	Humans(String sex,String name,int age){
		this.sex = sex;
		this.name = name;
		this.age = age;
	}

    public String getName() {
    	return name;
    }

    public void setName(String name) {
    	this.name = name;
    }

}

public class Student extends Humans{

	Student(String sex ,String name,int age){
		super(sex,name,age);
	}
}

public class test {
	public static void main(String args[]){
		Student s = new Student("男","zhangsan",10);
		System.out.println(s.sex);
		System.out.println(s.name);
		System.out.println(s.age);
	}
}

```
 上面的System.out.println(s.name);会报错，因为name是private属性，如需访问，采用get方法：

```java
System.out.println(s.getName());

输出结果为：
男
zhangsan
10
```
## 派生类的覆盖
如果派生类定义了和基类一样的属性或方法，将覆盖基类的属性和方法。

如将student改为如下代码：
```java
public class Student extends Humans{

	public String sex;

	protected int age ;

	private String name;

	Student(String sex ,String name,int age){
		super(sex,name,age);
	}

    public String getName() {
    	return name;
    }

    public void setName(String name) {
    	this.name = name;
    }
}
```
输出结果为:

```
输出结果为：

null

null

0
```
因为只有基类的属性在构造时赋值了，派生类的没有，当访问这些属性时，访问的是派生类的属性，所以全为null或者0。
# 重写与重载
## 重写(Override)
### 定义
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变

### 场景
使用 @Override 注解，可以让编译器帮忙检查是否满足上面的三个限制条件。

- 子类方法的访问权限必须大于等于父类方法；
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。。

### 调用父类被重写的方法

当需要在子类中调用父类的被重写方法时，要使用 super 关键字。

```java
class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}

class Dog extends Animal{
   public void move(){
      super.move(); // 应用super类的方法
      System.out.println("狗可以跑和走");
   }
}

public class TestDog{
   public static void main(String args[]){

      Animal b = new Dog(); // Dog 对象
      b.move(); //执行 Dog类的方法

   }
}

```

## 重写的向上向下转型
### 向上转型(子类向上转成父类来用)

子类以丢失掉父类中没有的方法为代价,但使用的是子类重写的方法,变成一个父类的实例来用

```java
package com.sheepmu;
 class Animal
 {
	public void eat()
	{
		System.out.println("父类的 eating...");
	}
}
class Bird extends Animal
{
	@Override
	public void eat()
	{
		System.out.println("子类重写的父类的  eatting...");
	}
	public void fly()
	{
		System.out.println("子类新方法  flying...");
	}
}

public class Sys
{
	public static void main(String[] args)
	{
		Animal b=new Bird(); //向上转型
		b.eat();
		//  b.fly(); b虽指向子类对象，但此时子类作为向上的代价丢失和父类不同的fly()方法

	}
}

```

输出:
```
子类重写的父类的  eatting...

Animal b=new Bird(); //向上转型
b.eat(); // 调用的是子类的eat()方法
b.fly(); // 报错!!!!!-------b虽指向子类对象，但此时子类作为向上转型的代价丢失和父类不同的fly()方法------
```

向上转型的好处:为何不直接Bird b=new Bird();b.eat() 呢？

降低了代码的可扩展性.若是不用向上转型，那么有多少个子类就得在这儿写多少种不同的Bird,Dog,Cat...的加工方法

### 向下转型

```java
package com.sheepmu;
 class Fruit
  {
	public void myName()
	{
		System.out.println("我是父类  水果...");
	}
}

class Apple extends Fruit
{
	@Override
	public void myName()
	{
		System.out.println("我是子类  苹果...");
	}
	public void myMore()
	{
		System.out.println("我是你的小呀小苹果~~~~~~");
	}
}

public class Sys{
	public static void main(String[] args) {
		Fruit a=new Apple(); //向上转型
		a.myName();

		Apple aa=(Apple)a; //向下转型,编译和运行皆不会出错(正确的)
		aa.myName();//向下转型时调用的是子类的
		aa.myMore();;

		Fruit f=new Fruit();
        Apple aaa=(Apple)f; //-不安全的---向下转型,编译无错但会运行会出错
  		aaa.myName();
  		aaa.myMore();
	}
}

输出:
我是子类  苹果...
我是子类  苹果...
我是你的小呀小苹果~~~~~~
Exception in thread "main" java.lang.ClassCastException: com.sheepmu.Fruit cannot be cast to com.sheepmu.Apple
at com.sheepmu.Sys.main(Sys.java:30)

详解：

1.正确的向下转型

                    Fruit a=new Apple(); //向上转型
                    a.myName();
                    Apple aa=(Apple)a; //向下转型,编译和运行皆不会出错(正确的)
                    aa.myName();
                    aa.myMore();
a指向子类的对象，所以子类的实例aa也可以指向a啊~~

向下转型后因为都是指向子类对象，所以调用的当然全是子类的方法~~

2.不安全的向下转型

               Fruit f=new Fruit();
               Apple aaa=(Apple)f; //-不安全的---向下转型,编译无错但会运行会出错
               aaa.myName();
                aaa.myMore();

f是父类对象，子类的实例aaa指向强转的父类,在编译时不报错,但运行时因为父类没有子类的方法,会导致aaa调用子类的方法出错

3.Java为了解决不安全的向下转型问题，引入泛型的概念

4.为了安全的类型转换，最好先用 if(A instanceof  B) 判断一下下~~

```
## 静态分派和动态分派

TODO

## 重载(Overload)
### 定义

重载(overloading) 是在一个类里面，方法名字相同，而参数个数,顺序,类型至少一个不同不同。

返回值不同不算重载

### 重载规则:

被重载的方法必须改变参数列表(参数个数或类型或顺序不一样)；

被重载的方法可以改变返回类型；

被重载的方法可以改变访问修饰符；

被重载的方法可以声明新的或更广的检查异常；

方法能够在同一个类中或者在一个子类中被重载。

无法以返回值类型作为重载函数的区分标准。

### 重载和重写的区别
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

另外还有 返回值,修饰符,抛出异常,参数列表方面的不同:

# Object 通用方法

## 概览

```java

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native Class<?> getClass()

protected void finalize() throws Throwable {}

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
```
## == 与 equal

==比较的是引用的内存地址

equal看类的equal实现情况

- 对于基本类型，== 判断两个值是否相等，基本类型没有 equals() 方法。

- Integer,Double等基本类型的包装类,==判断引用地址,equal方法比较的是先比较地址,若地址相同则返回true,否则比较内容

- String ==比较地址,equal方法比较的是先比较地址,若地址相同则返回true,否则比较内容

- StringBuilder,StringBuffer==比较地址,没有实现equal方法,继承自Object的equals,所以比较的仍然是地址
```java
  public boolean equals(Object obj) {
        return (this == obj);
    }
```

## hashCode()

hashCode() 返回散列值，而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同，但是散列值相同的两个对象不一定等价。

在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法，保证等价的两个对象散列值也相等。

下面的代码中，新建了两个等价的对象，并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的，只在集合中添加一个对象，但是因为 EqualExample 没有实现 hashCode() 方法，因此这两个对象的散列值是不同的，最终导致集合添加了两个等价的对象。

```java
EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2
```

理想的散列函数应当具有均匀性，即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位，然后组成一个 R 进制的整数。R 一般取 31，因为它是一个奇素数，如果是偶数的话，当出现乘法溢出，信息就会丢失，因为与 2 相乘相当于向左移一位。

一个数与 31 相乘可以转换成移位和减法：`31*x == (x<<5)-x`，编译器会自动进行这个优化。

```java
@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}
```

## toString()

默认返回 ToStringExample@4554617c 这种形式，其中 @ 后面的数值为散列码的无符号十六进制表示。

```java
public class ToStringExample {

    private int number;

    public ToStringExample(int number) {
        this.number = number;
    }
}
```

```java
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
```

```html
ToStringExample@4554617c
```

## clone()

**1. cloneable**

clone() 是 Object 的 protected 方法，它不是 public，一个类不显式去重写 clone()，其它类就不能直接去调用该类实例的 clone() 方法。

```java
public class CloneExample {
    private int a;
    private int b;
}
```

```java
CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
```

重写 clone() 得到以下实现：

```java
public class CloneExample {
    private int a;
    private int b;

    @Override
    public CloneExample clone() throws CloneNotSupportedException {
        return (CloneExample)super.clone();
    }
}
```

```java
CloneExample e1 = new CloneExample();
try {
    CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
```

```html
java.lang.CloneNotSupportedException: CloneExample
```

以上抛出了 CloneNotSupportedException，这是因为 CloneExample 没有实现 Cloneable 接口。

应该注意的是，clone() 方法并不是 Cloneable 接口的方法，而是 Object 的一个 protected 方法。Cloneable 接口只是规定，如果一个类没有实现 Cloneable 接口又调用了 clone() 方法，就会抛出 CloneNotSupportedException。

```java
public class CloneExample implements Cloneable {
    private int a;
    private int b;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
```

**2. 浅拷贝**

拷贝对象和原始对象的引用类型引用同一个对象。

```java
public class ShallowCloneExample implements Cloneable {

    private int[] arr;

    public ShallowCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected ShallowCloneExample clone() throws CloneNotSupportedException {
        return (ShallowCloneExample) super.clone();
    }
}
```

```java
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222
```

**3. 深拷贝**

拷贝对象和原始对象的引用类型引用不同对象。

```java
public class DeepCloneExample implements Cloneable {

    private int[] arr;

    public DeepCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected DeepCloneExample clone() throws CloneNotSupportedException {
        DeepCloneExample result = (DeepCloneExample) super.clone();
        result.arr = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            result.arr[i] = arr[i];
        }
        return result;
    }
}
```

```java
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
```

**4. clone() 的替代方案**

使用 clone() 方法来拷贝一个对象即复杂又有风险，它会抛出异常，并且还需要类型转换。Effective Java 书上讲到，最好不要去使用 clone()，可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

```java
public class CloneConstructorExample {

    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }
}
```

```java
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
```

# 关键字

## 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. 方法**

private声明方法不能被子类重写。

private 方法隐式地被指定为 final，如果在子类中定义的方法和基类中的一个 private 方法签名相同，此时子类的方法不是重写基类方法，而是在子类中定义了一个新的方法。

**3. 类**

声明类不允许被继承。

### 设计一个不可变的类

#### 不可变模式
对象创建之后它的属性值不能够发生变化！所有对原对象的操作都会返回原对象的拷贝。

设计一个不可变类应该遵循以下几点：

1、类的所有属性声明为private，去除掉所有的setter方法，防止外界直接对其进行修改
2、类的声明采用final进行修饰，保证没有父类对其修改
3、类的属性声明为final,如果对象类型为可变类型，应对其重新包装，重新new一个对象返回

优点：

**多线程环境中进行同步而不需要考虑线程同步**

缺点：

每次返回都创建新的对象，内存会有一定的开销，不容易被垃圾回收器回收，造成资源的浪费,不适合大对象、且创建频繁的场景，因为对象大且创建频繁会容易导致内存泄漏

```java
package com.wokao66;

/**
 * 不可变类
 * @author: huangjiawei
 * @since: 2018年4月2日
 * @version: $Revision$ $Date$ $LastChangedBy$
 *
 */
//采用fianl修饰，防止子类继承
public final class Immutable {

	/**
	 * 所有的属性private且final
	 */
	private final String name;
	private final int age;

	/**
	 * 构造方法
	 * @param name
	 * @param age
	 */
	public Immutable(String name, int age) {
		this.name = name;
		this.age = age;
	}

	/**
	 * 去除所有的setter方法
	 */
	public String getName() {
		return name;
	}

	public int getAge() {
		return age;
	}

	/**
	 * 将年龄增加10岁
	 * @param newAge
	 * @return
	 */
	public Immutable addAge(int newAge) {
		/**
		 * 重新返回一个对象
		 */
		return new Immutable(this.getName(), newAge + this.getAge());
	}

	public static void main(String[] args) {
		Immutable immutable = new Immutable("a", 12);
		System.err.println(immutable.getAge());
		Immutable newImmutable = immutable.addAge(10);
		System.err.println(immutable.getAge());
		System.err.println(newImmutable.getAge());
	}
}

```

## static

**1. 静态变量**

- 静态变量：又称为类变量，也就是说这个变量属于类的，类所有的实例都共享静态变量，可以直接通过类名来访问它。静态变量在内存中只存在一份。
- 实例变量：每创建一个实例就会产生一个实例变量，它与该实例同生共死。

```java
public class A {

    private int x;         // 实例变量
    private static int y;  // 静态变量

    public static void main(String[] args) {
        // int x = A.x;  // Non-static field 'x' cannot be referenced from a static context
        A a = new A();
        int x = a.x;
        int y = A.y;
    }
}
```

**2. 静态方法**

静态方法在类加载的时候就存在了，它不依赖于任何实例。所以静态方法必须有实现，也就是说它不能是抽象方法。

```java
public abstract class A {
    public static void func1(){
    }
    // public abstract static void func2();  // Illegal combination of modifiers: 'abstract' and 'static'
}
```

只能访问所属类的静态字段和静态方法，方法中不能有 this 和 super 关键字。

```java
public class A {

    private static int x;
    private int y;

    public static void func1(){
        int a = x;
        // int b = y;  // Non-static field 'y' cannot be referenced from a static context
        // int b = this.y;     // 'A.this' cannot be referenced from a static context
    }
}
```

**3. 静态语句块**

静态语句块在类初始化时运行一次。

```java
public class A {
    static {
        System.out.println("123");
    }

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}
```

```html
123
```

**4. 静态内部类**

非静态内部类依赖于外部类的实例，而静态内部类不需要。

```java
public class OuterClass {

    class InnerClass {
    }

    static class StaticInnerClass {
    }

    public static void main(String[] args) {
        // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
}
```

静态内部类不能访问外部类的非静态的变量和方法。

**5. 静态导包**

在使用静态变量和方法时不用再指明 ClassName，从而简化代码，但可读性大大降低。

```java
import static com.xxx.ClassName.*
```

**6. 初始化顺序**

静态变量和静态语句块优先于实例变量和普通语句块，静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

```java
public static String staticField = "静态变量";
```

```java
static {
    System.out.println("静态语句块");
}
```

```java
public String field = "实例变量";
```

```java
{
    System.out.println("普通语句块");
}
```

最后才是构造函数的初始化。

```java
public InitialOrderTest() {
    System.out.println("构造函数");
}
```

存在继承的情况下，初始化顺序为：

- 父类（静态变量、静态语句块）
- 子类（静态变量、静态语句块）
- 父类（实例变量、普通语句块）
- 父类（构造函数）
- 子类（实例变量、普通语句块）
- 子类（构造函数）

## native
[native参考博客](https://blog.csdn.net/xw13106209/article/details/6989415)
### 定义
使用native关键字说明这个方法是原生函数，也就是这个方法是用C/C++语言实现的，并且被编译成了DLL，由**java去调用**。 这些函数的**实现体在DLL中**，JDK的源代码中并不包含，你应该是看不到的。对于**不同的平台它们也是不同**的。这也是java的底层机制，实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。

###  JN的书写步骤
1. 编写带有native声明的方法的java类，生成.java文件
2. 使用javac命令编译所编写的java类，生成.class文件
3. 使用javah -jni java类名生成扩展名为h的头文件，也即生成.h文件
4. 使用C/C++（或者其他编程想语言）实现本地方法，创建.h文件的实现，也就是创建.cpp文件实现.h文件中的方法
5. 将C/C++编写的文件生成动态连接库，生成dll文件

.h文件包含函数声明和宏定义
其中,第三步,生成的.h文件里面有java规定的方法命名方式,比如

native函数所在源码
```java
public class HelloWorld {
	public native void displayHelloWorld();// java native方法申明

	static {
		System.loadLibrary("HelloWorldImpl");// 装入动态链接库，"HelloWorldImpl"是要装入的动态链接库名称。
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		HelloWorld helloWorld = new HelloWorld();
		helloWorld.displayHelloWorld();
	}
}
```

d:\JNI>javac HelloWorld.java之后,生成的.h文件的函数命名是有规律的:Java_HelloWorld_displayHelloWorld

```java
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorld
 * Method:    displayHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
```

# 反射
## 定义
在程序运行时可以知道和调用某个类的任意方法和属性的功能叫反射机制

每个类都有一个  **Class**  对象，包含了与类有关的信息。当编译一个新类时，会产生一个同名的 .class 文件，该文件内容保存着 Class 对象。

类加载相当于 Class 对象的加载，类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载，该方法会返回一个 Class 对象。

反射可以提供运行时的类信息，并且这个类可以在运行时才加载进来，甚至在编译时期该类的 .class 不存在也可以加载进来。

Class 和 java.lang.reflect 一起对反射提供了支持，java.lang.reflect 类库主要包含了以下三个类：

-  **Field** ：可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段；
-  **Method** ：可以使用 invoke() 方法调用与 Method 对象关联的方法；
-  **Constructor** ：可以用 Constructor 的 newInstance() 创建新的对象。

## 获取Class对象
1.  用Class的方法: Class.forName(driver);
2. 直接获取某一个对象的 class: Class<?> classInt = Integer.TYPE;
3. 调用某个对象的 getClass() 方法:Class<?> klass = str.getClass();

## 创建实例

使用Class对象的newInstance()方法来创建Class对象对应类的实例。

Class<?> c = String.class;
Object str = c.newInstance();

先通过Class对象获取指定的Constructor对象，再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。
```java
//获取String所对应的Class对象
Class<?> c = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
Object obj = constructor.newInstance("23333");
System.out.println(obj);
```

## 使用反射获取类信息

### 使用反射获取变量信息

```java
/**
 * 通过反射获取类的所有变量
 */
private static void printFields(){
    //1.获取并输出类的名称
    Class mClass = SonClass.class;
    System.out.println("类的名称：" + mClass.getName());

    //2.1 获取所有 public 访问权限的变量
    // 包括本类声明的和从父类继承的
    Field[] fields = mClass.getFields();

    //2.2 获取所有本类声明的变量（不问访问权限）
    //Field[] fields = mClass.getDeclaredFields();

    //3. 遍历变量并输出变量信息
    for (Field field :
            fields) {
        //获取访问权限并输出
        int modifiers = field.getModifiers();
        System.out.print(Modifier.toString(modifiers) + " ");
        //输出变量的类型及变量名
        System.out.println(field.getType().getName()
                 + " " + field.getName());
    }
}
```
调用 getFields() 方法，输出 SonClass 类以及其所继承的父类( 包括 FatherClass 和 Object ) 的 public 方法。
```java
  类的名称：obj.SonClass
  public java.lang.String mSonBirthday
  public java.lang.String mFatherName
  public int mFatherAge
```

调用 getDeclaredFields() ， 输出 SonClass 类的所有成员变量，不问访问权限。
```java
  类的名称：obj.SonClass
  private java.lang.String mSonName
  protected int mSonAge
  public java.lang.String mSonBirthday
```

### 使用反射获取方法信息

```java
/**
 * 通过反射获取类的所有方法
 */
private static void printMethods(){
    //1.获取并输出类的名称
    Class mClass = SonClass.class;
    System.out.println("类的名称：" + mClass.getName());

    //2.1 获取所有 public 访问权限的方法
    //包括自己声明和从父类继承的
    Method[] mMethods = mClass.getMethods();

    //2.2 获取所有本类的的方法（不问访问权限）
    //Method[] mMethods = mClass.getDeclaredMethods();

    //3.遍历所有方法
    for (Method method :
            mMethods) {
        //获取并输出方法的访问权限（Modifiers：修饰符）
        int modifiers = method.getModifiers();
        System.out.print(Modifier.toString(modifiers) + " ");
        //获取并输出方法的返回值类型
        Class returnType = method.getReturnType();
        System.out.print(returnType.getName() + " "
                + method.getName() + "( ");
        //获取并输出方法的所有参数
        Parameter[] parameters = method.getParameters();
        for (Parameter parameter:
             parameters) {
            System.out.print(parameter.getType().getName()
                    + " " + parameter.getName() + ",");
        }
        //获取并输出方法抛出的异常
        Class[] exceptionTypes = method.getExceptionTypes();
        if (exceptionTypes.length == 0){
            System.out.println(" )");
        }
        else {
            for (Class c : exceptionTypes) {
                System.out.println(" ) throws "
                        + c.getName());
            }
        }
    }
}
```

调用 getMethods() 方法  获取 SonClass 类所有 public 访问权限的方法，包括从父类继承的。打印信息中，printSonMsg() 方法来自 SonClass 类， printFatherMsg() 来自 FatherClass 类，其余方法来自 Object 类。

**Class类是一个类,里面有Filed,Method,Parameter等描述一个类的属性.**

```java
  类的名称：obj.SonClass
  public void printSonMsg(  )
  public void printFatherMsg(  )
  public final void wait(  ) throws java.lang.InterruptedException
  public final void wait( long arg0,int arg1, ) throws java.lang.InterruptedException
  public final native void wait( long arg0, ) throws java.lang.InterruptedException
  public boolean equals( java.lang.Object arg0, )
  public java.lang.String toString(  )
  public native int hashCode(  )
  public final native java.lang.Class getClass(  )
  public final native void notify(  )
  public final native void notifyAll(  )
```

调用 getDeclaredMethods() 方法

```java
  类的名称：obj.SonClass
  private int getSonAge(  )
  private void setSonAge( int arg0, )
  public void printSonMsg(  )
  private void setSonName( java.lang.String arg0, )
  private java.lang.String getSonName(  )
```

## 访问和操作私有变量与方法

### 访问私有方法
```java
/**
 * 访问对象的私有方法
 * 为简洁代码，在方法上抛出总的异常，实际开发别这样
 */
private static void getPrivateMethod() throws Exception{
    //1. 获取 Class 类实例
    TestClass testClass = new TestClass();
    Class mClass = testClass.getClass();

    //2. 获取私有方法
    //第一个参数为要获取的私有方法的名称
    //第二个为要获取方法的参数的类型，参数为 Class...，没有参数就是null
    //方法参数也可这么写 ：new Class[]{String.class , int.class}
    Method privateMethod =
            mClass.getDeclaredMethod("privateMethod", String.class, int.class);

    //3. 开始操作方法
    if (privateMethod != null) {
        //获取私有方法的访问权
        //只是获取访问权，并不是修改实际权限
        privateMethod.setAccessible(true);

        //使用 invoke 反射调用私有方法
        //privateMethod 是获取到的私有方法
        //testClass 要操作的对象
        //后面两个参数传实参
        privateMethod.invoke(testClass, "Java Reflect ", 666);
    }
}
```
### 修改私有变量
```java
/**
 * 修改对象私有变量的值
 * 为简洁代码，在方法上抛出总的异常
 */
private static void modifyPrivateFiled() throws Exception {
    //1. 获取 Class 类实例
    TestClass testClass = new TestClass();
    Class mClass = testClass.getClass();

    //2. 获取私有变量
    Field privateField = mClass.getDeclaredField("MSG");

    //3. 操作私有变量
    if (privateField != null) {
        //获取私有变量的访问权
        privateField.setAccessible(true);

        //修改私有变量，并输出以测试
        System.out.println("Before Modify：MSG = " + testClass.getMsg());

        //调用 set(object , value) 修改变量的值
        //privateField 是获取到的私有变量
        //testClass 要操作的对象
        //"Modified" 为要修改成的值
        privateField.set(testClass, "Modified");
        System.out.println("After Modify：MSG = " + testClass.getMsg());
    }
}
```
### 修改私有常量

对于基本类型,由于在编译的时候,已经把用到常量的地方 替换为具体常量值,所以无法修改;

但普通的引用类型常量,可以修改

编译前:
```java
//注意是 String  类型的值
private final String FINAL_VALUE = "hello";

if(FINAL_VALUE.equals("world")){
    //do something
}

```

编译后的.class文件

```java
private final String FINAL_VALUE = "hello";
//替换为"hello"
if("hello".equals("world")){
    //do something
}
```

## 应用场景

**反射的优点：**

*    **可扩展性**  ：应用程序可以利用全限定名创建可扩展对象的实例，来使用来自外部的用户自定义类。
*    **类浏览器和可视化开发环境**  ：一个类浏览器需要可以枚举类的成员。可视化开发环境（如 IDE）可以从利用反射中可用的类型信息中受益，以帮助程序员编写正确的代码。
*    **调试器和测试工具**  ： 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义，以确保一组测试中有较高的代码覆盖率。

**反射的缺点：**

尽管反射非常强大，但也不能滥用。如果一个功能可以不用反射完成，那么最好就不用。在我们使用反射技术时，下面几条内容应该牢记于心。

*    **性能开销**  ：反射涉及了动态类型的解析，所以 JVM 无法对这些代码进行优化。因此，反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。

*    **安全限制**  ：使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行，如 Applet，那么这就是个问题了。

*    **内部暴露**  ：由于反射允许代码执行一些在正常情况下不被允许的操作（比如访问私有的属性和方法），所以使用反射可能会导致意料之外的副作用，这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性，因此当平台发生改变的时候，代码的行为就有可能也随着变化。

# 异常
## 分类

Throwable 可以用来表示任何可以作为异常抛出的类，分为两种： **Error**  和 **Exception**。

**注意：异常(exception)和错误(error)的区别：异常能被程序本身处理，错误是无法处理。**

**Error**
用来表示 JVM 无法处理的错误，大多数错误与代码编写者执行的操作无关，而表示代码运行时 JVM（Java 虚拟机）出现的问题。例如，Java虚拟机运行错误（Virtual MachineError），当 JVM 不再有继续执行操作所需的内存资源时，将出现 OutOfMemoryError。这些异常发生时，Java虚拟机（JVM）一般会选择线程终止。

**Exception**
分为两种：

-  **受检异常/可查异常/非运行时异常/编译异常** ：需要用 try...catch... 语句捕获并进行处理,不捕获编译无法通过，并且可以从异常中恢复；
-  **非受检异常/不可查异常/运行时异常** ：是程序运行时错误,是由逻辑错误引起的,无法在编译期发现错误，例如除 0 会引发 Arithmetic Exception，此时程序崩溃并且无法恢复。

## 异常处理机制
在 Java 应用程序中，异常处理机制为：**抛出异常，捕捉异常。**

Error，当运行方法不欲捕捉时，Java允许该方法不做任何抛出声明。因为，大多数Error异常属于永远不能被允许发生的状况，也属于合理的应用程序不该捕捉的异常。

对于Exception中的可查异常：要么捕获,要么抛出

对于Exception中的运行时异常: 由于运行时异常的不可查性，运行时异常将由Java运行时系统自动抛出，允许应用程序忽略运行时异常。

### 异常捕获语句 try-catch
catch捕获

对应异常及其子类异常,

一经捕获不再经过其他catch,所以对于有多个catch子句的异常程序而言，应该尽量将捕获底层异常类的catch子句放在前面，同时尽量将捕获相对高层的异常类的catch子句放在后面。否则，捕获底层异常类的catch子句将可能会被屏蔽。
### 异常捕获语句 try－catch-finally
try-catch语句还可以包括第三部分，就是finally子句。它表示无论是否出现异常，都会执行的语句.

## 执行顺序
1. try、catch、finally语句块的执行顺序:

1)当try没有捕获到异常时：try语句块中的语句逐一被执行，程序将跳过catch语句块，执行finally语句块和其后的语句；

2)当try捕获到异常，catch语句块里没有处理此异常的情况：当try语句块里的某条语句出现异常时，而没有处理此异常的catch语句块时，此异常将会抛给JVM处理，finally语句块里的语句还是会被执行，但finally语句块后的语句不会被执行；

3)当try捕获到异常，catch语句块里有处理此异常的情况：在try语句块中是按照顺序来执行的，当执行到某一条语句出现异常时，程序将跳到catch语句块，并与catch语句块逐一匹配，找到与之对应的处理程序，其他的catch语句块将不会被执行，而try语句块中，出现异常之后的语句也不会被执行，catch语句块执行完后，执行finally语句块里的语句，最后执行finally语句块后的语句；

## 异常链
Java方法抛出的可查异常将依据调用栈、沿着方法调用的层次结构一直传递到具备处理能力的调用方法，最高层次到main方法为止。

## 常见异常

**runtimeException子类:**

java.lang.ArrayIndexOutOfBoundsException
数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。

java.lang.ArithmeticException
算术条件异常。譬如：整数除零等。

java.lang.NullPointerException
空指针异常。当应用试图在要求使用对象的地方使用了null时，抛出该异常。譬如：调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等

java.lang.ClassNotFoundException
找不到类异常。当应用试图根据字符串形式的类名构造类，而在遍历CLASSPAH之后找不到对应名称的class文件时，抛出该异常。

java.lang.NegativeArraySizeException  数组长度为负异常

java.lang.ArrayStoreException 数组中包含不兼容的值抛出的异常

java.lang.SecurityException 安全性异常

java.lang.IllegalArgumentException 非法参数异常

**IOException**

IOException：操作输入流和输出流时可能出现的异常。

EOFException   文件已结束异常

FileNotFoundException   文件未找到异常

**其他**

ClassCastException    类型转换异常类

ArrayStoreException  数组中包含不兼容的值抛出的异常

SQLException   操作数据库异常类

NoSuchFieldException   字段未找到异常

NoSuchMethodException   方法未找到抛出的异常

NumberFormatException    字符串转换为数字抛出的异常

StringIndexOutOfBoundsException 字符串索引超出范围抛出的异常

IllegalAccessException  不允许访问某类异常

InstantiationException  当应用程序试图使用Class类中的newInstance()方法创建一个类的实例，而指定的类对象无法被实例化时，抛出该异常

# 泛型
## 定义

泛型就是把   类型   参数化,使类型可以灵活变动

```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
class 类名称 <泛型标识：可以随便写任意标识号，标识指定的泛型的类型>{
  private 泛型标识 /*（成员变量类型）*/ var;
  .....

  }
}
```
举例
```java
//此处T可以随便写为任意标识，常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时，必须指定T的具体类型
public class Generic<T>{
    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T，T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T，T的类型由外部指定
        return key;
    }
}
```

## 泛型接口
举例
```java
//定义一个泛型接口
public interface Generator<T> {
    public T next();
}
```

实现泛型接口举例,未传入泛型实参时：
```java
/**
 * 未传入泛型实参时，与泛型类的定义相同，在声明类的时候，需将泛型的声明也一起加到类中
 * 即：class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型，如：class FruitGenerator implements Generator<T>，编译器会报错："Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}
```
当实现泛型接口的类，传入泛型实参时：
```java

/**
 * 传入泛型实参时：
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参，形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时，如已将泛型类型传入实参类型，则所有使用泛型的地方都要替换成传入的实参类型
 * 即：Generator<T>，public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
```

## 泛型方法
泛型方法的使用比 泛型接口和泛型类要复杂,好好看实例:
```java
/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明：
 *     1）public 与 返回值中间<T>非常重要，可以理解为声明此方法为泛型方法
 *     2）只有声明了<T>的方法才是泛型方法，泛型类中的使用了泛型的成员方法并不是泛型方法
 *     3）<T>表明该方法将使用泛型类型T，此时才可以在方法中使用泛型类型T
 *     4）与泛型类的定义一样，此处T可以随便写为任意标识，常见的如T、E、K、V等形式的参数常用于表示泛型
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}
```

## 泛型通配符
### 通配符介绍

类型通配符用?表示,它跟其他传入的integer,string一样**是一个类型实参,而不是类型形参.**可以把它看成是所有类型的父类.

之所以有泛型统配符是因为在使用T做泛型时:
```java
//泛型类
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}
//为泛型Number准备的方法,不能给Integer使用
public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

//验证如下:
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);
//即使Number是Integer的父类,为Number准备的方法"showKeyValue"并不能给
//Integer泛型的方法使用
// showKeyValue这个方法编译器会为我们报错：Generic<java.lang.Integer>
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
```
即使Number是Integer的父类,为Number准备的方法"showKeyValue"并不能给Integer泛型的方法使用

改成通配符,就可以把这个方法给Integer和Number来使用了
```java
public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
```

### 通配符的上下边界
**Plate<？ extends Fruit>**
一个能放水果以及一切是水果子类(香蕉,苹果)的盘子。

**Plate<？ super Fruit>**
一个能放水果以及一切是水果基类也就是父类(食物类,Object类)的盘子。

**PECS原则**
最后看一下什么是PECS（Producer Extends Consumer Super）原则，已经很好理解了：
频繁往外读取内容的，适合用上界Extends。
因为保证了粒度最大也用有Fruit的信息,所以读取出来能保留Fruit的一些特性.但如果是存的话,就要分门别类的存,很麻烦.

经常往里插入的，适合用下界Super。
因为保证了最小粒度是Fruit,所以存Food类,Object类都是可以存储的.但是取的时候如果取出来的是Object类不转型,就会丢失很多信息.

## 泛型擦除

泛型在编译的时候,会被具体的类型替换掉,所以在装入jvm之后,我们能拿到的本来是泛型类型信息T,变成了Object(如果规定了泛型下界,比如 T extends String ,那么拿到的就是下界String)

举例:
```java
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();

System.out.println(l1.getClass() == l2.getClass());

打印的结果为 true 是因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。

反编译的结果如下
public class GenericEraseTest {
	public static void main(String[] args){
		ArrayList stringList = new ArrayList();
		ArrayList intList = new ArrayList();

		System.out.println(stringList.getClass() == intList.getClass());
	}
}

```
在C++或者C#中，泛型无论是在源码，还是在编译的中间代码，亦或者是在运行期中，泛型都是真实存在的，我们都可以正常的使用它，List<String>和List<Integer>就是两个不同的类，但是在java中并不是这样的。

### 泛型擦除的优缺点
优点
提高编译速度,List<String>和List<Integer>将生成两个不同的类，很容易导致类膨胀的问题，使得代码编译的速度降低。

缺点
无法在运行的代码中获取到泛型的类型信息,所以不能再创建实例

### java中没有泛型数组的原因

java中数组的缺陷:
Object[]数组可以是任何数组的父类，或者说，任何一个数组都可以向上转型成它在定义时指定元素类型的父类的数组，这个时候如果我们往里面放不同于原始数据类型 但是满足后来使用的父类类型的话，编译不会有问题，但是在运行时会检查加入数组的对象的类型，于是会抛ArrayStoreException：

```java
//创建了一个String数组
String[] strArray = new String[20];
//把数组转成了Object类型
Object[] objArray = strArray;
//再往数组中添加一个Integer类型,编译时不会报错,运行时会报错
objArray[0] = new Integer(1); // throws ArrayStoreException at runtime

```
因为java的泛型会在运行时擦除类型信息,上面数组至少在运行时可以被检查出错误,而泛型数组由于类型信息被擦除,往mapArray中存放map<String,String>运行时也不能检查出错误,直到系统崩溃
```java
Map<Integer, String>[] mapArray = new Map<Integer, String>[20];
```

# 注解
## 定义
注解是一种描述数据的修饰符,实际上Java注解与普通修饰符(public、static、void等)的使用方式
## 原理
注解本质是一个继承了Annotation的特殊接口，其具体实现类是Java运行时生成的动态代理类。通过代理对象调用自定义注解（接口）的方法，会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的生成是来源于添加注解的那个类的常量池,里面存储的是注解方法名+注解方法对应的值.

[Java注解（Annotation）原理详解](https://blog.csdn.net/lylwo317/article/details/52163304)
## 使用场景
注解可以用来修饰类、方法、参数等等，具体的使用场景有以下三种：

编译前提示信息：注解可以被编译器用来发现错误，或者清除不必要的警告；

编译时生成代码：一些处理器可以在编译时根据注解信息生成代码，比如 Java 代码，xml 代码等；

运行时处理：我们可以在运行时根据注解，通过反射获取具体信息，然后做一些操作,比如组装数据库Sql语句。

## 常见元注解
**@Target注解传入ElementType.METHOD参数来标明@Test只能用于方法上**

```java
public enum ElementType {
    /**标明该注解可以用于类、接口（包括注解类型）或enum声明*/
    TYPE,

    /** 标明该注解可以用于字段(域)声明，包括enum实例 */
    FIELD,

    /** 标明该注解可以用于方法声明 */
    METHOD,

    /** 标明该注解可以用于参数声明 */
    PARAMETER,

    /** 标明注解可以用于构造函数声明 */
    CONSTRUCTOR,

    /** 标明注解可以用于局部变量声明 */
    LOCAL_VARIABLE,

    /** 标明注解可以用于注解声明(应用于另一个注解上)*/
    ANNOTATION_TYPE,

    /** 标明注解可以用于包声明 */
    PACKAGE,

    /**
     * 标明注解可以用于类型参数声明（1.8新加入）
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * 类型使用声明（1.8新加入)
     * @since 1.8
     */
    TYPE_USE
}
```

**@Retention(RetentionPolicy.RUNTIME)则用来表示该注解生存期是运行时，**
@Retention用来约束注解的生命周期，分别有三个值，源码级别（source），类文件级别（class）或者运行时级别（runtime），其含有如下：

SOURCE：注解将被编译器丢弃（该类型的注解信息只会保留在源码里，源码经过编译后，注解信息会被丢弃，不会保留在编译好的class文件里）

CLASS：注解在class文件中可用，但会被VM丢弃（该类型的注解信息会保留在源码里和class文件里，在执行的时候，不会加载到虚拟机中），请注意，当注解未定义Retention值时，默认值是CLASS，如Java内置注解，@Override、@Deprecated、@SuppressWarnning等

RUNTIME：注解信息将在运行期(JVM)也保留，因此可以通过反射机制读取注解的信息（源码、class文件和执行的时候都有注解的信息），如SpringMvc中的@Controller、@Autowired、@RequestMapping等。
# 内部变量

# 内部类
## 定义
内部类是指在一个外部类的内部再定义一个类。内部类作为外部类的一个成员，并且依附于外部类而存在的。内部类可为静态，可用protected和private修饰（而外部类只能使用public和缺省的包访问权限）。内部类主要有以下几类：成员内部类、局部内部类、静态内部类、匿名内部类
## 特点
(1)内部类仍然是一个独立的类，在编译之后内部类会被编译成独立的.class文件，但是前面冠以外部类的类名和$符号 。

(2)内部类不能用普通的方式访问。

(3)内部类声明成静态的，就不能随便的访问外部类的成员变量了，此时内部类只能访问外部类的静态成员变量   。

(4)外部类不能直接访问内部类的的成员，但可以通过内部类对象来访问

(5)内部类是外部类的一个成员，因此内部类可以自由地访问外部类的成员变量，无论是否是private的。

## 作用
内部类能够独立继承一个接口,抽象类的实现,无论外部类是否已经继承了这个接口,使得多重继承的解决方案变得更加完整。

## 内部类的分类
内部类是个编译时的概念，一旦编译成功后，它就与外围类属于两个完全不同的类（当然他们之间还是有联系的）。对于一个名为OuterClass的外围类和一个名为InnerClass的内部类，在编译成功后，会出现这样两个class文件：OuterClass.class和OuterClass$InnerClass.class

内部类可以无限制的访问外部类的变量和方法,

### 成员内部类
成员内部类也是最普通的内部类，它是外围类的一个成员，所以他是可以无限制的访问外围类的所有 成员属性和方法，尽管是private的，但是外围类要访问内部类的成员属性和方法则需要通过内部类实例来访问。

在成员内部类中要注意两点，第一：成员内部类中不能存在任何static的变量和方法；第二：成员内部类是依附于外围类的，所以只有先创建了外围类才能够创建内部类。

### 局部内部类

局部内部类作用域方法和属性

定义在方法里
```java
public class Parcel5 {
    public Destionation destionation(String str){
        class PDestionation implements Destionation{
            private String label;
            private PDestionation(String whereTo){
                label = whereTo;
            }
            public String readLabel(){
                return label;
            }
        }
        return new PDestionation(str);
    }

    public static void main(String[] args) {
        Parcel5 parcel5 = new Parcel5();
        Destionation d = parcel5.destionation("chenssy");
    }
}
```

定义在作用域里

```java
public class Parcel6 {
    private void internalTracking(boolean b){
        if(b){
            class TrackingSlip{
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip(){
                    return id;
                }
            }
            TrackingSlip ts = new TrackingSlip("chenssy");
            String string = ts.getSlip();
        }
    }

    public void track(){
        internalTracking(true);
    }

    public static void main(String[] args) {
        Parcel6 parcel6 = new Parcel6();
        parcel6.track();
    }
}
```

### 静态内部类

1、 它的创建是不需要依赖于外围类的。

2、 它不能使用任何外围类的非static成员变量和方法。

### 匿名内部类

匿名内部类只能使用一次,下次使用要重新创建

匿名内部类不能存在构造函数,可以使用构造代码块来进行初始化

匿名内部类不能是抽象的，它必须要实现继承的类或者实现的接口的所有抽象方法。

匿名内部类中不能存在任何的静态成员变量和静态方法。

举例:
```java
public abstract class Bird {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public abstract int fly();
}

public class Test {

    public void test(Bird bird){
        System.out.println(bird.getName() + "能够飞 " + bird.fly() + "米");
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.test(new Bird() {

            public int fly() {
                return 10000;
            }

            public String getName() {
                return "大雁";
            }
        });
    }
}
------------------
Output：
大雁能够飞 10000米
```

这段匿名内部类代码可以拆解成为以下形式

```java
public class WildGoose extends Bird{
    public int fly() {
        return 10000;
    }

    public String getName() {
        return "大雁";
    }
}

WildGoose wildGoose = new WildGoose();
test.test(wildGoose);
```
### 匿名内部类的形参要使用final

匿名内部类在编译成功后,与外围类是两个独立的.class文件,匿名内部类并没有直接引用外围类传给匿名内部类的参数,而是在自己的.class文件里面又创建了一份.所以为了保证外围类中传给内部类的形参的值  与  内部类自己内部的形参的值保持一致,所以要使用final;

举例:
```java

首先我们知道在内部类编译成功后，它会产生一个class文件，
该class文件与外部类并不是同一class文件，仅仅只保留对
外部类的引用。当外部类传入的参数需要被内部类调用时，
从java程序的角度来看是直接被调用：

public class OuterClass {
    public void display(final String name,String age){
        class InnerClass{
            void display(){
                System.out.println(name);
            }
        }
    }
}

从上面代码中看好像name参数应该是被内部类直接调用？
其实不然，在java编译之后内部类的情况如下：

public class OuterClass$InnerClass {

    public InnerClass(String name,String age){
        this.InnerClass$name = name;
        this.InnerClass$age = age;
    }
    public void display(){
        System.out.println(this.InnerClass$name + "----" + this.InnerClass$age );
    }

}
```
所以从上面代码来看，内部类内部方法调用的实际上时自己的属性而不是外部方法传递进来的参数。

直到这里还没有解释为什么是final？在内部类中的属性和外部方法的参数两者从外表上看是同一个东西，但实际上却不是，所以他们两者是可以任意变化的，也就是说在内部类中我对属性的改变并不会影响到外部的形参，而然这从程序员的角度来看这是不可行的，毕竟站在程序的角度来看这两个根本就是同一个，如果内部类该变了，而外部方法的形参却没有改变这是难以理解和不可接受的，所以为了保持参数的一致性，就规定使用final来避免形参的不改变。

# 特性
## 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.Object，C++ 为了兼容 C 即支持面向对象也支持面向过程。
- Java 通过虚拟机从而实现跨平台特性，但是 C++ 依赖于特定的平台。
- Java 没有指针，它的引用可以理解为安全指针，而 C++ 具有和 C 一样的指针。
- Java 支持自动垃圾回收，而 C++ 需要手动回收。
- Java 不支持多重继承，只能通过实现多个接口来达到相同目的，而 C++ 支持多重继承。
- Java 不支持操作符重载，虽然可以对两个 String 对象执行加法运算，但是这是语言内置支持的操作，不属于操作符重载，而 C++ 可以。
- Java 的 goto 是保留字，但是不可用，C++ 可以使用 goto。
- Java 不支持条件编译，C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。

[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"

# Java根类Object的方法

Java中的Object类是所有类的父类，它提供了以下11个方法：

```
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 { }
```

## getClass方法
getClass()也是一个native方法，返回的是此Object对象的类对象/运行时类对象Class<?>。效果与Object.class相同。

首先解释下"类对象"的概念：在Java中，类是是对具有一组相同特征或行为的实例的抽象并进行描述，对象则是此类所描述的特征或行为的具体实例。作为概念层次的类，其本身也具有某些共同的特性，如都具有类名称、由类加载器去加载，都具有包，具有父类，属性和方法等。于是，Java中有专门定义了一个类，Class，去描述其他类所具有的这些特性，因此，从此角度去看，类本身也都是属于Class类的对象。为与经常意义上的对象相区分，在此称之为"类对象"。

## equals方法
这个方法是可以自己定义的,"等于"的标准取决于自己
## hashCode方法

哈希码的通用约定如下：

1. 在java程序执行过程中，在一个对象没有被改变的前提下，无论这个对象被调用多少次，hashCode方法都会返回相同的整数值。对象的哈希码没有必要在不同的程序中保持相同的值。
2. 如果2个对象使用equals方法进行比较并且相同的话，那么这2个对象的hashCode方法的值也必须相等。
3. 如果根据equals方法，得到两个对象不相等，那么这2个对象的hashCode值不需要必须不相同。但是，不相等的对象的hashCode值不同的话可以提高哈希表的性能。

根据第二三条,重写equals就必须重写hashCode.

## clone方法

创建并返回当前对象的一份拷贝。一般情况下，对于任何对象 x，表达式 x.clone() != x 为true，x.clone().getClass() == x.getClass() 也为true。

Object类的clone方法是一个protected的native方法。

由于Object本身没有实现Cloneable接口，所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

深拷贝拷贝堆内存,浅拷贝拷贝栈指针.

## toString方法
toString()是由对象的类型和其哈希码唯一确定，同一类型但不相等的两个对象分别调用toString()方法返回的结果可能相同。

## notify方法

唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果所有的线程都在此对象上等待，那么只会选择一个线程。选择是任意性的，并在对实现做出决定时发生。一个线程在对象监视器上等待可以调用wait方法。

因为notify只能在拥有对象监视器的所有者线程中调用，否则会抛出IllegalMonitorStateException异常

## notifyAll方法
跟notify一样，唯一的区别就是会唤醒在此对象监视器上等待的所有线程，而不是一个线程。

同样，如果当前线程不是对象监视器的所有者，那么调用notifyAll同样会发生IllegalMonitorStateException异常。

## wait(long timeout) throws InterruptedException方法

wait(long timeout)方法同样是一个native方法，并且也是final的，不允许子类重写。

wait方法会让当前线程等待直到另外一个线程调用对象的notify或notifyAll方法，或者超过参数设置的timeout超时时间。

跟notify和notifyAll方法一样，当前线程必须是此对象的监视器所有者，否则还是会发生IllegalMonitorStateException异常。

线程T是不可用并处于休眠状态，直到发生以下四件事中的任意一件：

其他某个线程调用此对象的notify方法，并且线程T碰巧被任选为被唤醒的线程
其他某个线程调用此对象的notifyAll方法
其他某个线程调用Thread.interrupt方法中断线程T
时间到了参数设置的超时时间。如果timeout参数为0，则不会超时，会一直进行等待

## finalize方法
finalize方法是一个protected方法，Object类的默认实现是不进行任何操作

该方法的作用是实例被垃圾回收器回收的时候触发的操作，就好比 “死前的最后一波挣扎”

# 常见扩容

1.初始  2.扩容后的倍数.

StringBuilder len+16,2+1

ArrayList 10,  1.5+1

map   16  装填因子  2倍。

<file_sep>/codes/javacode/jichu/Trim.java
package javacode.jichu;

/**
 * @author <EMAIL>
 * @date 19/9/22/022 14:32
 */
public class Trim {

    public static void main(String[] args) {
        String a = null;
        String b = myTrim(a);
        System.out.println(b);

        a.toLowerCase();
        a.compareTo("asd");
        a.isEmpty();
        new String();

    }

    public static String myTrim(String string) {
        if (string == null || string.length() < 1) {
            return "";
        }
        char[] chars = string.toCharArray();
        int start = 0;
        int end = chars.length-1;

        while (end >= 0 && chars[end] == ' ') {
            end--;
        }
        while (start <= chars.length-1 && chars[start] == ' ') {
            start++;
        }
        if (end < start) {
            return "";
        } else {
            //0,1,2,3,4,5
            //后面end+1 是因为substring 左闭右开,用的是数组下标
            //为什么是左闭右开  因为最后是new String (value,start,sublen);
            return string.substring(start, end+1);
        }
    }
}
<file_sep>/codes/javacode/jichu/PriorityQueueTopHeap.java
package javacode.jichu;

import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * @author <EMAIL>
 * @date 19/9/16/016 15:46
 */
public class PriorityQueueTopHeap {

    public static void main(String[] args) {

//        PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(10, new Comparator<Integer>() {
//
//            //默认逻辑是o1-o2
//            //默认是小顶堆
//            //这个时候把默认比较逻辑取反,就得到了大顶堆
//            @Override
//            public int compare(Integer o1, Integer o2) {
//                return o2-o1;
//            }
//
//        });

        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(10, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return 0;
            }
        });
        maxHeap.offer(1);
        maxHeap.offer(4);
        maxHeap.offer(9);
        maxHeap.offer(3);
        System.out.println(maxHeap.poll());
        //peek是只查不删  poll是查完删除
        System.out.println(maxHeap.peek());
        System.out.println(maxHeap.poll());
    }
}<file_sep>/codes/javacode/jichu/StringTest.java
package javacode.jichu;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 *
 *
 *@author <EMAIL>
 *@date 19/8/22/022 13:05
 */

public class StringTest {

    public static String BASEINFO = "Mr.Y";
    public static final int COUNT = 2000000;

    /**
     * 执行一项String赋值测试
     */
    public static void doStringTest() {

        String str = new String(BASEINFO);
        long starttime = System.currentTimeMillis();
        for (int i = 0; i < COUNT / 100; i++) {
            str = str + "miss";
        }
        long endtime = System.currentTimeMillis();
        System.out.println((endtime - starttime)
                + " millis has costed when used String.");
    }

    /**
     * 执行一项StringBuffer赋值测试
     */
    public static void doStringBufferTest() {

        StringBuffer sb = new StringBuffer(BASEINFO);
        long starttime = System.currentTimeMillis();
        for (int i = 0; i < COUNT; i++) {
            sb = sb.append("miss");
        }
        long endtime = System.currentTimeMillis();
        System.out.println((endtime - starttime)
                + " millis has costed when used StringBuffer.");
    }

    /**
     * 执行一项StringBuilder赋值测试
     */
    public static void doStringBuilderTest() {

        StringBuilder sb = new StringBuilder(BASEINFO);
        long starttime = System.currentTimeMillis();
        for (int i = 0; i < COUNT; i++) {
            sb = sb.append("miss");
        }
        long endtime = System.currentTimeMillis();
        System.out.println((endtime - starttime)
                + " millis has costed when used StringBuilder.");
    }

    /**
     * 测试StringBuffer遍历赋值结果
     *
     * @param mlist
     */
    public static void doStringBufferListTest(List<String> mlist) {
        StringBuffer sb = new StringBuffer();
        long starttime = System.currentTimeMillis();
        for (String string : mlist) {
            sb.append(string);
        }
        long endtime = System.currentTimeMillis();
        System.out.println(sb.toString() + "buffer cost:"
                + (endtime - starttime) + " millis");
    }

    /**
     * 测试StringBuilder迭代赋值结果
     *
     * @param mlist
     */
    public static void doStringBuilderListTest(List<String> mlist) {
        StringBuilder sb = new StringBuilder();
        long starttime = System.currentTimeMillis();
        for (Iterator<String> iterator = mlist.iterator(); iterator.hasNext();) {
            sb.append(iterator.next());
        }

        long endtime = System.currentTimeMillis();
        System.out.println(sb.toString() + "builder cost:"
                + (endtime - starttime) + " millis");
    }

    public static void main(String[] args) {
        doStringTest();
        doStringBufferTest();
        doStringBuilderTest();

        List<String> list = new ArrayList<String>();
        list.add(" I ");
        list.add(" like ");
        list.add(" BeiJing ");
        list.add(" tian ");
        list.add(" an ");
        list.add(" men ");
        list.add(" . ");

        doStringBufferListTest(list);
        doStringBuilderListTest(list);
    }

}

//输出结果
//
//2711 millis has costed when used String.
//211 millis has costed when used StringBuffer.
//141 millis has costed when used StringBuilder.
// I  like  BeiJing  tian  an  men  . buffer cost:1 millis
// I  like  BeiJing  tian  an  men  . builder cost:0 millis
//
//```<file_sep>/notes/视频管理系统.md

<!-- TOC -->

- [功能描述](#功能描述)
    - [功能](#功能)
    - [图文积压定时警报](#图文积压定时警报)
    - [host白名单](#host白名单)
    - [业务描述](#业务描述)
    - [云剪辑](#云剪辑)
    - [评论补充弹幕](#评论补充弹幕)
    - [用户封禁](#用户封禁)
    - [弹幕屏蔽词](#弹幕屏蔽词)
    - [用户视频播放量定时更新](#用户视频播放量定时更新)
    - [发送通知](#发送通知)
        - [single通知 对notice topic代码的研究](#single通知-对notice-topic代码的研究)
        - [multi 开协程发送通知](#multi-开协程发送通知)
        - [广播模式 databus代码](#广播模式-databus代码)
- [源码研读](#源码研读)
    - [视频源码](#视频源码)
        - [稿件表每个字段的设计意义](#稿件表每个字段的设计意义)
        - [过滤的实现](#过滤的实现)
        - [svid的生成原因](#svid的生成原因)
        - [转码的实现](#转码的实现)
        - [转码url是如何做的](#转码url是如何做的)
- [总结经验](#总结经验)
    - [发布流程](#发布流程)
    - [思路分析](#思路分析)
    - [高并发](#高并发)
    - [全量广播中断](#全量广播中断)
    - [埋点](#埋点)
    - [操作记录](#操作记录)
    - [热加载](#热加载)
    - [富文本的注入解决](#富文本的注入解决)
    - [权限校验](#权限校验)
    - [全量发送](#全量发送)
    - [ios推送通知是富文本格式](#ios推送通知是富文本格式)
    - [重复操作的幂等](#重复操作的幂等)
    - [实例的负载均衡](#实例的负载均衡)
    - [用户输入的东西未必是完全可信的](#用户输入的东西未必是完全可信的)
    - [XSS攻击](#xss攻击)
    - [CSRF](#csrf)
    - [UAT和UT](#uat和ut)
        - [UAT(验收测试，User Acceptance Test)](#uat验收测试user-acceptance-test)
        - [UT(单元测试，Unit Test)](#ut单元测试unit-test)
        - [IT（集成测试，Integration Test）](#it集成测试integration-test)
        - [ST（系统测试，System Test）](#st系统测试system-test)
    - [Mock](#mock)
    - [定义](#定义)
        - [好处](#好处)
    - [gorm](#gorm)
- [传值还是传地址](#传值还是传地址)
- [canal](#canal)
    - [原理](#原理)
    - [应用](#应用)
- [restful 和 rpc 区别](#restful-和-rpc-区别)
    - [用途](#用途)
- [grpc](#grpc)
    - [dubbo使用tcp的原因](#dubbo使用tcp的原因)
    - [grpc的优势](#grpc的优势)
    - [grpc 原理](#grpc-原理)
        - [简单 RPC调用流程](#简单-rpc调用流程)
        - [grpc的api使用](#grpc的api使用)
    - [Request](#request)
    - [Response](#response)
    - [Protobuf](#protobuf)
- [http1  和 http 2](#http1--和-http-2)
    - [http1.x](#http1x)
    - [http2](#http2)
        - [Frame组成](#frame组成)
    - [stream特性](#stream特性)
    - [HPACK(头部简化)](#hpack头部简化)
    - [FlowControl](#flowcontrol)

<!-- /TOC -->

# 主要亮点

分表的方案，checkpoint ，幂等性的处理再细节一点。app保证幂等性，app定时发消息，消息进缓存。做一个定点的缓存，listnotice来控制消息。 qps 5000

# 功能描述

## 功能

## 图文积压定时警报

bug  uat和local一起抢占databus
     redis.do的reply 用redis.bool来判断,结果返回的reply的意义是count;当count为0的时候会报会退出,导致失败

```go

func (d *Dao) GetEarliestTaskTimeDb(ctx context.Context, workflowId int16) (t *time.Time, err error) {
	sqlStr := fmt.Sprintf(_sqlTaskEarliestTime)
	t = new(time.Time)
	row := d.db.QueryRow(ctx, sqlStr, workflowId)
	err = row.Scan(t)
	if err == sql.ErrNoRows {
		return t, nil
	}
	return
}

func (d *Dao) GetTaskCountDb(ctx context.Context, workFlowId int16) (count int, err error) {
	row := d.db.QueryRow(ctx, _sqlTaskStackCount, workFlowId)
	err = row.Scan(&count)
	if err != nil {
		log.Errorc(ctx, "GetTaskCountDb Failed ,error:%v", err)
		return
	}
	return
}

func (d *Dao) SetTaskCountRedis(ctx context.Context, workflowId int16, count int) (int, error) {
	key := fmt.Sprintf(TaskStackCount, workflowId)
	conn := d.redis.Get(ctx)
	defer conn.Close()

	reply, err := conn.Do("SET", key, count)

	if reply != "OK" || err != nil {
		log.Errorw(ctx, "SetTaskCountRedis error", err)
		return -1, err
	}
	_, err = conn.Do("EXPIRE", key, 86400)
	if err != nil {
		log.Errorw(ctx, "SetTaskCountRedis,set expire error:", err)
		return -1, err
	}
	count, err = redis.Int(conn.Do("GET", key))
	if err != nil {
		log.Errorc(ctx, "get TaskStackCount error:%#v", err)
	}
	return count, err
}

func (d *Dao) GetTaskStackCountRedis(ctx context.Context, workflowId int16) (count int, err error) {
	key := fmt.Sprintf(TaskStackCount, workflowId)
	conn := d.redis.Get(ctx)
	defer conn.Close()

	//TODO失效之后去 重置一个数据?
	count, err = redis.Int(conn.Do("GET", key))
	if err != nil {
		log.Errorc(ctx, "Redis Get TaskStackCount Failed ,error:%v", err)
		return
	}
	return
}

func (d *Dao) TaskStackCountIncre(ctx context.Context, workFlow int16) (count int, err error) {
	key := fmt.Sprintf(TaskStackCount, workFlow)
	conn := d.redis.Get(ctx)
	defer conn.Close()

	reply, err := conn.Do("INCR", key)
	if count, err = redis.Int(reply, err); err != nil {
		log.Errorw(ctx, "TaskStackCountIncre", "error", err)
		return
	}
	_, err = conn.Do("EXPIRE", key, 86400)
	if err != nil {
		log.Errorw(ctx, "TaskStackCountIncre,set expire error:", err)
	}

	log.Info("count:%d,workFlow:%d", count, workFlow)
	return count, err
}

func (d *Dao) TaskStackCountDecr(ctx context.Context, workFlow int16, decCount int) (count int, err error) {
	key := fmt.Sprintf(TaskStackCount, workFlow)
	conn := d.redis.Get(ctx)
	defer conn.Close()

	if count, err = redis.Int(conn.Do("DECRBY", key, decCount)); err != nil {
		log.Errorw(ctx, "event", "TaskStackCountDecr", "error", err, "count", count)
	}
	_, err = conn.Do("EXPIRE", key, 86400)
	if err != nil {
		log.Errorw(ctx, "TaskStackCountDecr,set expire error:", err)
	}

	log.Info("count:%d,workFlow:%d", count, workFlow)
	return count, err
}

```
## host白名单
建表
```sql
CREATE TABLE `white_host` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一主键',
  `host` varchar(255) NOT NULL DEFAULT '' COMMENT '白名单域名',
  `description` varchar(1000) NOT NULL DEFAULT '' COMMENT '域名描述',
  `status` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '域名的状态',
  `ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间',
  `mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `ix_mtime` (`mtime`),
  KEY `ix_host` (`host`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT '域名白名单记录表'

```

其中 mtime有两个属性，分别是CURRENT_TIMESTAMP 和ON UPDATE CURRENT_TIMESTAMP两种，使用情况分别如下：

1. CURRENT_TIMESTAMP

当要向数据库执行insert操作时，如果有个timestamp字段属性设为 CURRENT_TIMESTAMP，则无论这个字段有没有set值都插入当前系统时间

2. ON UPDATE CURRENT_TIMESTAMP
当执行update操作是，并且字段有ON UPDATE CURRENT_TIMESTAMP属性。则字段无论值有没有变化，它的值也会跟着更新为当前UPDATE操作时的时间。

AUTO_INCREMENT=1000是从1000开始增长

```sql
ALTER TABLE white_host ADD UNIQUE (host);
```
host修改为unique,白名单数据没必要重复

## 业务描述
bbq  cms admin  负责 增加 删除 获取所有host;
增加和删除的时候要删除rediskey;
获取的时候先获取redis,获取不到就数据库,然后更新库

bbq app interface  调用admin  返回给前端host列表
## 云剪辑
长视频剪辑,存标志位,然后存短视频,视频在云端剪好,主站返回svid等信息
## 评论补充弹幕

## 用户封禁

## 弹幕屏蔽词

## 用户视频播放量定时更新

## 发送通知

cms-admin-notice
notice-service
user-service

单播的时候同步发送
多播的时候开一个协程
广播的时候,把要广播的信息发送到databus上,交给一个consumer来运行

广播,单播,多播通知:
noticebase的信息有:
点赞的时候会发送通知
notice 部分  发送通知后,

redis记录未读通知
### single通知 对notice topic代码的研究

### multi 开协程发送通知

```go
func (s *Service) NoticeMultiUser(c context.Context, req *api.NoticeMultiUserReq) (empty *empty.Empty, err error) {
	midList := req.MidList

	midChan := s.midChanProducer(midList)

	//开一百个notice协程
	for i := 0; i < 100; i++ {
		err = s.midChanConsumer(midChan, req.Text, req.JumpUrl)
		//TODO 有error可以回收吗?
		if err != nil {
			log.Error("ConcurrentNoticeMultiUser Failed error:%#v", err)
			return
		}
	}
	return
}

//开一个协程把mids放入管道
func (s *Service) midChanProducer(mids []int64) (midChan chan int64) {
	midChan = make(chan int64, 100)
	go func() {
		defer close(midChan)
		for _, v := range mids {
			midChan <- v
		}
	}()
	return
}

//开一个协程消费管道中的mid,发送notice给user
//TODO 要不要context
func (s *Service) midChanConsumer(midChan <-chan int64, text string, jumpUrl string) (err error) {
	go func() {
		for mid := range midChan {
			//fmt.Printf("%d 消费了 %d", flag, mid)
			noticeBase := &notice.NoticeBase{
				Text:       text,
				Mid:        mid,
				NoticeType: notice.NoticeTypeSysMsg,
				BizType:    notice.NoticeBizTypeSysMsg,
				JumpUrl:    jumpUrl,
			}
			ctx := context.Background()
			err = s.dao.CreateSingleNotice(ctx, noticeBase)
			if err != nil {
				log.Errorw(ctx, "event", "ConcurrentNoticeMultiUser Failed", "error", err)
				return
			}
		}
	}()
	return
}
```
### 广播模式 databus代码

### 幂等性

### 进度记载

### 消息总线的提交特点

我用的消息总线主要是redis,没有用到kafka

分区+offset的hashMap存在redis里面

commit的时候,对应分区比offset小的全都commit了

所以需要先单线程放到管道里,然后并发消费.

# 源码研读
## 视频源码
service 初始化的时候拿到host,把host经过hash操作后,存储到redis,保证每台主机的dao是唯一的不冲突的

稿件表的设计:

for循环监听,message chan;databus订阅主站稿件变动消息,对消息进行查封,拆成newVideo和oldVideo,和action和table

如果是update,判断是否是删除动态,如果是删除动态就同步把自己的视频展示等级设为不可见;还有一种可能是insert,这两种情况都要通过过滤(过滤规则要加载,有tid过滤,up主过滤,视频属性过滤版权最大最小时长比特率主题);过滤完之后

生成svid;由时间戳mid 等构成

创建一个topicClient  现在还不知道是干嘛用的.

把稿件传递给生产流程,通过稿件里面的dynamic(稿件的表设计为什么这样),提取tags,然后稿件信息传给转码的producerClient(可以画图的结构关系client)

在grpc调用的BVCXCodeCommit中,主要做三件事:生成video记录到数据表,提交转码请求(发送一个hhtp请求,包含svid和cid给配置好的请求地址);就可以对视频进行转码,然后返回结果是视频转码,写回稿件表

### 稿件表每个字段的设计意义

### 过滤的实现

### svid的生成原因

### 转码的实现

### 转码url是如何做的

# 总结经验

## 发布流程

写代码,写日志,本地debug,发到uat,uat测试通过,code review,发到pre,pre测试通过,发到pro,测试通过,删除次要日志,留下关键日志(info里也有很多关键日志),merge到master(merge交给别人来做)

线上发版要提测,跟别人说

平时没事就pull一下,

## 思路分析

自动驾驶汽车放几个摄像头,怎么查发给了多少个用户,不知道就去分析,分析一下就变成知道了

## 高并发

高并发是好事,但是createnotice高并发的是数据库,导致数据库压力过大爆503

## 全量广播中断

broadcastNoitce运行时间很长,会导致长时间无法做msgCommit,这个时候如果全量广播中断,下次再广播依然会消费没有被commit的消息,

```go
func (s *Service) pushNotice(msg *databus.Message) {
	var (
		dm *model.NoticeBaseDatabus
	)
	//因为broadcast占用时间比较长,如果中途停掉,再次开启服务还是会广播,造成多次广播,后面应该加广播幂等性
	defer msg.Commit()
	if err := jsoniter.Unmarshal(msg.Value, &dm); err != nil {
		log.Error("pushNotice Failed")
		return
	}
	if dm.Broadcast {
		log.Info("broadcastNotice:%#v", dm)
		err := s.broadcastNotice(dm)
		if err != nil {
			log.Error("broadcastNotice fail ")
			return
		}
		return
	}
	for _, mid := range dm.MidList {
		log.Info("notBroadcastNotice:%#v", dm)
		noticeBase := &v1.NoticeBase{
			Text:       dm.Text,
			Mid:        mid,
			NoticeType: v1.NoticeTypeSysMsg,
			BizType:    v1.NoticeBizTypeSysMsg,
			JumpUrl:    dm.JumpUrl,
		}
		s.noticeBaseChan <- noticeBase
	}
}
```

## 埋点

## 操作记录

## 热加载
## 富文本的注入解决
<p>req.text<p>  如果text也是富文本

## 权限校验

## 全量发送
如果一个出错,不要return 而是 continue
```go

func (s *Service) run() (err error) {
	for noticeBase := range s.noticeBaseChan {
		_, err = s.CreateNotice(context.Background(), noticeBase)
		if err != nil {
			log.Errorw(context.Background(), "event", "noticeBaseChanConsumer Failed", "error", err)
			continue
		}
	}
	return
}
```
## ios推送通知是富文本格式
如果只用text字体会很小
## 重复操作的幂等
去重 唯一id (幂等性)
## 实例的负载均衡
再推到消息总线上,负载均衡来做

## 用户输入的东西未必是完全可信的
判空 判断格式
```go
if strings.Trim(req.Text, " ") == "" {
		err = errors.New("text不能为空")
		log.Error("SendNoticeDatabus Failed error:%#v", err)
		return
	}
```

## XSS攻击

人们经常将跨站脚本攻击（Cross Site Scripting）缩写为CSS，但这会与层叠样式表（Cascading Style Sheets，CSS）的缩写混淆。因此，有人将跨站脚本攻击缩写为XSS。

XSS攻击通常指的是通过利用网页开发时留下的漏洞，通过巧妙的方法注入恶意指令代码到网页，使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript，

详情博客:
https://blog.csdn.net/qq_36119192/article/details/82469035
## CSRF

用户在未登出A网站的前提下,cookie保留,登陆了恶意网站B,B网站利用还未失效的cookie冒充用户访问A网站.

解决办法:
cookie加密

详情博客:
https://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html

## UAT和UT
### UAT(验收测试，User Acceptance Test)
验收测试是向未来的用户表明系统能够像预定要求那样工作。

经集成测试后，已经按照设计把所有的模块组装成一个完整的软件系统，接口错误也已经基本排除了，接着就应该进一步验证软件的有效性，这就是验收测试的任务，即软件的功能和性能如同用户所合理期待的那样。

### UT(单元测试，Unit Test)
单元测试任务包括：

模块接口测试；
模块局部数据结构测试；
模块边界条件测试；
模块中所有独立执行通路测试；
模块的各条错误处理通路测试。

### IT（集成测试，Integration Test）
也称系统集成测试（System Integration Test）或结合测试。
集成测试阶段是以黑盒法为主，在自底向上集成的早期，白盒法测试占一定的比例，随着集成测试的不断深入，这种比例在测试过程中将越来越少，渐渐地，黑盒法测试占据主导地位。

### ST（系统测试，System Test）
从技术角度看，系统测试是整个测试阶段的最后一步，所有的开发和测试在这一点上集中表现为生成一个具有一定功能的软件系统。
该阶段主要对系统的准确性及完整性等方面进行测试。

主要进行：
功能确认测试、运行测试、强度测试、恢复测试、安全性测试等。

系统测试的测试人员由测试组成员（或质量保证人员）或测试组成员与用户共同测试。在整个系统开发完成，即将交付用户使用前进行。在这一阶段，完全采用黑盒法对整个系统进行测试。

## Mock
https://juejin.im/post/5bd82d796fb9a05d25682a66
## 定义
按照事先规定的接口规范,返回虚拟的数据
### 好处
当使用mock之后，各团队之间可以不需要再互相等待对方的进度，只需要约定好相互之间的数据规范（文档），即可使用mock构建一个可用的接口，然后尽快的进行开发和调试以及自测，提升开发进度的的同时，也将发现缺陷的时间点大大提前。

复现某些接口返回值

提前开始单元测试

## gorm
废弃gorm,反直觉而且有性能损耗

#  传值还是传地址

go只有值传递,
传指针则是复制一个指针,可以操作传入参数指向的内存
传值则是拷贝一份值
传切片也分传值和传地址:
```go
package main

import (
    "fmt"
)

func main() {
    array := [5]int{0, 1, 2, 3, 4}
    slice := array[:]
    fmt.Println(slice)
    fmt.Printf("slice in main1 %p \n", &slice)
    appendTest(slice)
    fmt.Printf("slice in main2 %p \n", &slice)
    fmt.Println(slice)
    fmt.Println("=============")
    fmt.Println(slice)
    fmt.Printf("slice in main1 %p \n", &slice)
    appendTestRef(&slice)
    fmt.Printf("slice in main2 %p \n", &slice)
    fmt.Println(slice)
}

func appendTest(slice []int) {
    fmt.Printf("slice appendTest1 %p \n", slice)
    slice = append(slice, 5)
    fmt.Println(slice)
    fmt.Printf("slice appendTest2 %p \n", slice)
}

func appendTestRef(slice *[]int) {
    fmt.Printf("slice appendTestRef1 %p \n", slice)
    *slice = append(*slice, 5)
    fmt.Println(slice)
    fmt.Printf("slice appendTestRef2 %p \n", slice)
}
```
```
输出
    [0 1 2 3 4]
    slice in main1 0xc082008520
    slice appendTest1 0xc08200be90
    [0 1 2 3 4 5]
    slice appendTest2 0xc08200e1e0
    slice in main2 0xc082008520
    [0 1 2 3 4]
    =============
    [0 1 2 3 4]
    slice in main1 0xc082008520
    slice appendTestRef1 0xc082008520
    &[0 1 2 3 4 5]
    slice appendTestRef2 0xc082008520
    slice in main2 0xc082008520
    [0 1 2 3 4 5]
```

# canal
canal是纯Java开发的，基于数据库增量日志解析，提供增量数据订阅&消费，目前主要支持了mysql。

## 原理

1.      canal模拟mysql slave的交互协议，伪装自己为mysql slave，向mysql master发送dump协议

2.      mysql master收到dump请求，开始推送binary log给slave(也就是canal)

3.      canal解析binary log对象(原始为byte流)

## 应用
canal监听多个表的binlog,解析完成之后,送到kafka的生产者组group-P,topic是cmsTask

再由kafka的对应消费者组group-C,topic也是cmsTask来消费

# restful

RESTful(Representational State Transfer 具象状态传输)是一种设计风格

1. REST 是面向资源的,对资源的操作与它无关，操作是通过 HTTP动词来体现，所以REST 通过 URI 暴露资源时，会强调不要在 URI 中出现动词。
```
GET /rest/api/getDogs --> GET /rest/api/dogs 获取所有小狗狗
GET /rest/api/addDogs --> POST /rest/api/dogs 添加一个小狗狗
GET /rest/api/editDogs/:dog_id --> PUT /rest/api/dogs/:dog_id 修改一个小狗狗
GET /rest/api/deleteDogs/:dog_id --> DELETE /rest/api/dogs/:dog_id 删除一个小狗狗
```
2. 每一个URI代表1种资源；
3. 客户端使用GET、POST、PUT、DELETE4个表示操作方式的动词对服务端资源进行操作：GET用来获取资源，POST用来新建资源（也可以用于更新资源），PUT用来更新资源，DELETE用来删除资源；
4. 通过操作资源的表现形式来操作资源；
5. 资源的表现形式是XML或者HTML；
6. 客户端与服务端之间的交互在请求之间是无状态的，从客户端到服务端的每个请求都必须包含理解请求所必需的信息。

## RESTful与RPC的用途区别

RESTful API：主要用在为第三方提供调用自家系统的一种途径。(客户端)

RPC：主要用在自家系统之间的互相调用，即实现系统的分布式。(微服务之间的调用)

gRPC可以方便地支持流式通信

# RPC

简介:
RPC 样式的 Web 服务客户端将一个装满数据的信封（包括方法和参数信息）通过 HTTP 发送到服务器。服务器打开信封并使用传入参数执行指定的方法。方法的结果打包到一个信封并作为响应发回客户端。客户端收到响应并打开信封。

每个对象都有自己独特的方法以及仅公开一个 URI 的 RPC 样式 Web 服务，URI 表示单个端点。它忽略 HTTP 的大部分特性且**仅支持 POST 方法**。

# dubbo(rpc框架)

## 使用tcp的原因

http的头部很占字节.

**自定义tcp协议可以极大地简化传输头内容。**

但http2.0协议已经优化编码效率问题，像grpc这种rpc库使用的就是http2.0协议。

#  grpc(rpc框架)

传输协议包含: 如著名的 gRPC 使用的 http2 协议，也有如dubbo一类的自定义报文的tcp协议。
编码协议包含: 如二进制编码的 protobuf binpack ,基于文本编码的 xml json等。

## grpc的优势
gRPC可以通过protobuf来定义接口，从而可以有更加严格的接口约束条件(约束更强)

protobuf可以将数据序列化为二进制编码，这会大幅减少需要传输的数据量，从而大幅提高性能。(性能更高)

gRPC 通常有四种模式，unary，client streaming，server streaming 以及 bidirectional streaming，对于底层 HTTP/2 来说，它们都是 stream，并且仍然是一套 request + response 模型。

## grpc 原理

grpc通过protocol buffers 的 messages 定义数据结构,定义好数据结构后，可以使用 protocol buffers 编译器(protoc)将其生成其他语言的类。这些类提供所有属性的访问方法，及序列化和反序列方法。例如，你使用的是 C++，运行编译器会生成一个 Person 类，你可以在在你的应用中实例化或序列化来传输这些 protocol buffers 信息。

除了数据结构外，你还可以在 .proto 文件中定义 gRPC 服务，包括服务接口的参数和返回类型。所以除了会生成共传播和序列化的类以外，还会生成 gRPC client 和 server 代码。

### 简单 RPC调用流程
当 client 调用 RPC 方法时，会通知 server 对应的 RPC 方法被调用，包括 client 调用的元数据信息，方法名，以及 deadline
server 也需要通知 client 自己的元数据(这步发生在所有响应之前)，可以直接通知，也可以等收到客户端第一个请求时发送
当 server 接到 client 的请求后，根据请求和业务逻辑生成响应，这些响应包括状态码及payload，并返回给 client
如果状态 OK， client 会收到响应，并在 client 侧完成这次调用

### grpc的api使用
gRPC 使用 .proto 生成 client 和 server 代码及 API。client 调用这些 API, server 实现这些API。

server 侧实现服务声明的方法，并运行 gRPC server 来处理 client 的调用。gRPC 帮助你反序列化请求，执行服务方法，序列化响应并返回给 client.
client 侧 gRPC 会生成一个 client，这个 client 提供和 server 侧一样的接口供用户调用，并且使用 protocol buffer 序列化用户的请求参数，发送给 server，然后反序列化 server 的响应。

## Request
gRPC 的 request 通常包含 Request-Headers, 0 或者多个 Length-Prefixed-Message 以及 EOS。
Request-Headers 直接使用的 HTTP/2 headers，在 HEADERS 和 CONTINUATION frame 里面派发。定义的 header 主要有 Call-Definition 以及 Custom-Metadata。Call-Definition 里面包括 Method（其实就是用的 HTTP/2 的 POST），Content-Type 等。而 Custom-Metadata 则是应用层自定义的任意 key-value，key 不建议使用 grpc- 开头，因为这是为 gRPC 后续自己保留的。

Length-Prefixed-Message 主要在 DATA frame 里面派发，它有一个 Compressed flag 用来表示改 message 是否压缩，如果为 1，表示该 message 采用了压缩，而压缩算啊定义在 header 里面的 Message-Encoding 里面。然后后面跟着四字节的 message length 以及实际的 message。

EOS（end-of-stream） 会在最后的 DATA frame 里面带上了 END_STREAM 这个 flag。用来表示 stream 不会在发送任何数据，可以关闭了。

## Response
Response 主要包含 Response-Headers，0 或者多个 Length-Prefixed-Message 以及 Trailers。如果遇到了错误，也可以直接返回 Trailers-Only。
Response-Headers 主要包括 HTTP-Status，Content-Type 以及 Custom-Metadata 等。Trailers-Only 也有 HTTP-Status ，Content-Type 和 Trailers。Trailers 包括了 Status 以及 0 或者多个 Custom-Metadata。

HTTP-Status 就是我们通常的 HTTP 200，301，400 这些，很通用就不再解释。Status 也就是 gRPC 的 status， 而 Status-Message 则是  gRPC 的 message。Status-Message 采用了 Percent-Encoded 的编码方式

如果在最后收到的 HEADERS frame 里面，带上了 Trailers，并且有 END_STREAM 这个 flag，那么就意味着 response 的 EOS。

## Protobuf

### Protobuf是什么

Protobuf实际是一套类似Json或者XML的数据传输格式和规范，用于不同应用或进程之间进行通信时使用。通信时所传递的信息是通过Protobuf定义的message数据结构进行打包，然后编译成二进制的码流再进行传输或者存储。

### Protobuf的序列化原理

推荐阅读的博客

:https://www.jianshu.com/p/4b987ef826d3

https://blog.csdn.net/carson_ho/article/details/70568606

<img src="pictures/视频管理系统/image-20200412180041354.png" alt="image-20200412180041354" style="zoom:50%;" />

### Protobuf的优点

相比较而言，Protobuf有如下优点：

- 足够简单
- 序列化后体积很小:消息大小只需要XML的1/10 ~ 1/3
- 解析速度快:解析速度比XML快20 ~ 100倍
- 多语言支持
- 更好的兼容性,Protobuf设计的一个原则就是要能够很好的支持向下或向上兼容

gRPC 的 service 接口是基于 protobuf 定义的，我们可以非常方便的将 service 与 HTTP/2 关联起来。

Path : /Service-Name/{method name}
Service-Name : ?( {proto package name} "." ) {service name}
Message-Type : {fully qualified proto message name}
Content-Type : "application/grpc+proto"

###

# http1  和 http 2

## http1.x
HTTP/1.x 协议是一个文本协议，可读性非常好，但其实并不高效

交互模式，一个连接每次只能一问一答，也就是client 发送了 request 之后，必须等到 response，才能继续发送下一次请求。如果需要同时进行大量的交互，client 需要跟 server 建立多条连接，但连接的建立也是有开销的，所以为了性能，通常这些连接都是**长连接一直保活**的，虽然对于 server 来说同时处理百万连接也没啥太大的挑战，但终归效率不高。
## http2
HTTP/2 是一个二进制协议，这也就意味着它的可读性几乎为 0,但可以解析

Stream： 一个双向流，一条连接可以有多个 streams。
Message： 也就是逻辑上面的 request，response。
Frame:：数据传输的最小单位。每个 Frame 都属于一个特定的 stream 或者整个连接。一个 message 可能有多个 frame 组成。

### Frame组成
```
+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+
```
Length：也就是 Frame 的长度，默认最大长度是 16KB，如果要发送更大的 Frame，需要显示的设置 max frame size。

Type：Frame 的类型，譬如有 DATA，HEADERS，PRIORITY 等。Flag 和 R：保留位，可以先不管。

Stream Identifier：标识所属的 stream，如果为 0，则表示这个 frame 属于整条连接。

Frame Payload：根据不同 Type 有不同的格式。

## stream特性
一条连接可以包含多个 streams，多个 streams 发送的数据互相不影响。

Stream 可以被 client 和 server 单方面或者共享使用。

Stream 可以被任意一段关闭。

Stream 会确定好发送 frame 的顺序，另一端会按照接受到的顺序来处理。

Stream 用一个唯一 ID 来标识。

stream 有优先级，方便对端为不同的请求分配不同的资源。譬如对于一个 Web 站点来说，优先加载重要的资源，而对于一些不那么重要的图片啥的，则使用低的优先级。

## HPACK(头部简化)

HPACK 提供了一个静态和动态的 table，静态 table 定义了通用的 HTTP header fields，譬如 method，path 等。发送请求的时候，只要指定 field 在静态 table 里面的索引，双方就知道要发送的 field 是什么了。

对于动态 table，初始化为空，如果两边交互之后，发现有新的 field，就添加到动态 table 上面，这样后面的请求就可以跟静态 table 一样，只需要带上相关的 index 就可以了。

同时，为了减少数据传输的大小，使用 Huffman 进行编码。这里就不再详细说明 HPACK 和 Huffman 如何编码了。
## FlowControl
HTTP/2 支持流控，如果 sender 端发送数据太快，receiver 端可能因为太忙，或者压力太大，或者只想给特定的 stream 分配资源，receiver 端就可能不想处理这些数据。

**特性:**
Flow control 是单向的。Receiver 可以选择给 stream 或者整个连接设置 window size。

Flow control 是基于信任的。Receiver 只是会给 sender 建议它的初始连接和 stream 的 flow control window size。

Flow control 不可能被禁止掉。当 HTTP/2 连接建立起来之后，client 和 server 会交换 SETTINGS frames，用来设置 flow control window size。

Flow control 是 hop-by-hop，并不是 end-to-end 的，也就是我们可以用一个中间人来进行 flow control。

# md5

参考博客: https://www.jianshu.com/p/81c30781d4f7

## md5 原理

md5将整个文件当做一个大文本信息，通过不可逆的字符串变换算法，产生一个唯一的MD5信息摘要。文件的md5类似于人的指纹，在世界上是独立无二的，如果任何人对文件做了任何改动，其md5的值也就是对应的“数字指纹”都会发生变化。

对MD5算法简要的叙述可以为：MD5以512位分组来处理输入的信息，且每一分组又被划分为16个32位子分组，经过了一系列的处理后，算法的输出由四个32位分组组成，将这四个32位分组级联后将生成一个128位散列值。

md5与对称和非对称加密算法不同，这两种密码是防止信息被窃取，而摘要算法的目标是用于证明原文的完整性。

## md5 用途

**密码的加密存储**，用户设置密码时，服务端只记录这个密码的MD5，而不记录密码本身，以后验证用户身份时，只需要将用户输入的密码再次做一下MD5后，与记录的MD5作一个比较即可验证其密码的合法性。

**数字签名**，比如发布一个程序，为了防止别人在你的程序里插入病毒或木马，你可以在发布这个程序的同时，公开这个程序文件的MD5码，这样别人只需要在任何地方下载这个程序后做一次MD5，然后跟公开的这个MD5作一个比较就知道这个程序是否被第三方修改过。

**文件完整性验证**，比如当下载一个文件时，服务器返回的信息中包括这个文件的md5，在本地下载完毕时进行md5，将两个md5值进行比较，如果一致则说明文件完整没有丢包现象。

**文件上传**，比如百度云实现的秒传，就是对比你上传的文件md5在百度服务器是否已经存在了。

go里面生成md5

```go
data := []byte(str)
has := md5.Sum(data)
md5str1 := fmt.Sprintf("%x", has) //将[]byte转成16进制

fmt.Println(md5str1)
```

## 对称加密和非对称加密

### 对称加密

一方通过密钥将信息加密后，把密文传给另一方，另一方通过这个相同的密钥将密文解密，转换成可以理解的明文。

甲和乙通过同一个 钥匙 加密解密

常见的对称加密算法有DES、3DES、AES、Blowfish、IDEA、RC5、RC6。

### 非对称加密

每个人都有一个公钥和私钥,公钥谁都可以知道,私钥自己维护.

发给甲的消息被其他人用甲的公钥加密,且只有甲的私钥可以把消息解密.

甲发出的消息都会被甲的密钥加密,只有其他人只有用甲的公钥才能解密

过程如下:

```
李四：
“hi哥们，我想给你发个密文，把你的公钥给我发过来用用。”

张三：
“没问题的，这是我的公钥： <KEY>，用这个公钥加密你的信息后给我发过来吧”

李四：
“这是我想对你说的话： *&#@uehuu(**#eehu&$##bfeu&&”

张三收到消息后,用自己的私钥把消息解密
```

常用的非对称加密算法：RSA

非对称加密算法应用非常广泛，SSH, HTTPS, TLS，电子证书，电子签名，电子身份证等等。

### 根证书

(文中有浏览器甲和正规网站乙和认证机构丙的例子)参考博客:https://segmentfault.com/a/1190000012466003

非对称加密算法解决了甲乙之间通信的消息是以密文的形式传递.这样黑客窃取到公钥也没法解密传递的消息.

但是解决不了有人冒充甲或者乙.

所以需要第三方丙.乙想要跟甲通话,需要先找丙,把自己的个人信息和公钥交给丙,丙用自己的私钥加密得到数字证书返回给乙,然后乙拿着丙返回的数字证书给甲展示,甲找丙,要来丙的公钥,解密用丙的私钥加密的乙的个人信息和乙的公钥,甲就可以验证乙的数字证书,

这个时候可以核对乙的个人信息+乙的公钥

核对通过后甲再给乙发送消息.<file_sep>/notes/HTTP.md
<!-- TOC -->

- [1. 一 、基础概念](#1-%e4%b8%80-%e5%9f%ba%e7%a1%80%e6%a6%82%e5%bf%b5)
  - [1.1. 简介](#11-%e7%ae%80%e4%bb%8b)
  - [1.2. 工作原理](#12-%e5%b7%a5%e4%bd%9c%e5%8e%9f%e7%90%86)
  - [1.3. 特点](#13-%e7%89%b9%e7%82%b9)
- [2. 知识结构(接下来几章将按照本章顺序详细介绍各个知识点)](#2-%e7%9f%a5%e8%af%86%e7%bb%93%e6%9e%84%e6%8e%a5%e4%b8%8b%e6%9d%a5%e5%87%a0%e7%ab%a0%e5%b0%86%e6%8c%89%e7%85%a7%e6%9c%ac%e7%ab%a0%e9%a1%ba%e5%ba%8f%e8%af%a6%e7%bb%86%e4%bb%8b%e7%bb%8d%e5%90%84%e4%b8%aa%e7%9f%a5%e8%af%86%e7%82%b9)
  - [2.1. 请求报文(分为请求行,请求头,请求体)](#21-%e8%af%b7%e6%b1%82%e6%8a%a5%e6%96%87%e5%88%86%e4%b8%ba%e8%af%b7%e6%b1%82%e8%a1%8c%e8%af%b7%e6%b1%82%e5%a4%b4%e8%af%b7%e6%b1%82%e4%bd%93)
    - [2.1.1. 请求行(分为请求方法,URL,请求协议)](#211-%e8%af%b7%e6%b1%82%e8%a1%8c%e5%88%86%e4%b8%ba%e8%af%b7%e6%b1%82%e6%96%b9%e6%b3%95url%e8%af%b7%e6%b1%82%e5%8d%8f%e8%ae%ae)
    - [2.1.2. 请求头(重要属性,比如content-type)](#212-%e8%af%b7%e6%b1%82%e5%a4%b4%e9%87%8d%e8%a6%81%e5%b1%9e%e6%80%a7%e6%af%94%e5%a6%82content-type)
    - [2.1.3. 请求体](#213-%e8%af%b7%e6%b1%82%e4%bd%93)
  - [2.2. 响应报文](#22-%e5%93%8d%e5%ba%94%e6%8a%a5%e6%96%87)
- [3. HTTP 方法](#3-http-%e6%96%b9%e6%b3%95)
  - [3.1. GET](#31-get)
  - [3.2. HEAD](#32-head)
  - [3.3. POST](#33-post)
  - [3.4. PUT](#34-put)
  - [3.5. PATCH](#35-patch)
  - [3.6. DELETE](#36-delete)
  - [3.7. OPTIONS](#37-options)
  - [3.8. CONNECT](#38-connect)
  - [3.9. TRACE](#39-trace)
- [4.GET 和 POST 比较](#4get-%e5%92%8c-post-%e6%af%94%e8%be%83)
  - [4.1. 作用](#41-%e4%bd%9c%e7%94%a8)
  - [4.2. 参数](#42-%e5%8f%82%e6%95%b0)
  - [4.3. 安全](#43-%e5%ae%89%e5%85%a8)
  - [4.4. 幂等性](#44-%e5%b9%82%e7%ad%89%e6%80%a7)
  - [4.5. 可缓存](#45-%e5%8f%af%e7%bc%93%e5%ad%98)
  - [4.6. XMLHttpRequest](#46-xmlhttprequest)
  - [4.7. TCP底层的区别](#47-tcp%e5%ba%95%e5%b1%82%e7%9a%84%e5%8c%ba%e5%88%ab)
- [5. URI](#5-uri)
- [6. HTTP/2.0](#6-http20)
  - [6.1. HTTP/1.x 缺陷](#61-http1x-%e7%bc%ba%e9%99%b7)
  - [6.2. 二进制分帧层](#62-%e4%ba%8c%e8%bf%9b%e5%88%b6%e5%88%86%e5%b8%a7%e5%b1%82)
  - [6.3. 服务端推送](#63-%e6%9c%8d%e5%8a%a1%e7%ab%af%e6%8e%a8%e9%80%81)
  - [6.4. 首部压缩](#64-%e9%a6%96%e9%83%a8%e5%8e%8b%e7%bc%a9)
  - [6.5. HTTP/1.1 新特性](#65-http11-%e6%96%b0%e7%89%b9%e6%80%a7)
- [7. HTTP 首部](#7-http-%e9%a6%96%e9%83%a8)
  - [7.1. 通用首部字段](#71-%e9%80%9a%e7%94%a8%e9%a6%96%e9%83%a8%e5%ad%97%e6%ae%b5)
  - [7.2. 请求首部字段](#72-%e8%af%b7%e6%b1%82%e9%a6%96%e9%83%a8%e5%ad%97%e6%ae%b5)
  - [7.3. 响应首部字段](#73-%e5%93%8d%e5%ba%94%e9%a6%96%e9%83%a8%e5%ad%97%e6%ae%b5)
  - [7.4. 实体首部字段](#74-%e5%ae%9e%e4%bd%93%e9%a6%96%e9%83%a8%e5%ad%97%e6%ae%b5)
- [8. 响应状态码](#8-%e5%93%8d%e5%ba%94%e7%8a%b6%e6%80%81%e7%a0%81)
  - [8.1. XX 信息](#81-xx-%e4%bf%a1%e6%81%af)
  - [8.2. XX 成功](#82-xx-%e6%88%90%e5%8a%9f)
  - [8.3. XX 重定向](#83-xx-%e9%87%8d%e5%ae%9a%e5%90%91)
  - [8.4. XX 客户端错误](#84-xx-%e5%ae%a2%e6%88%b7%e7%ab%af%e9%94%99%e8%af%af)
  - [8.5. XX 服务器错误](#85-xx-%e6%9c%8d%e5%8a%a1%e5%99%a8%e9%94%99%e8%af%af)
- [9. 具体应用](#9-%e5%85%b7%e4%bd%93%e5%ba%94%e7%94%a8)
  - [9.1. 连接管理](#91-%e8%bf%9e%e6%8e%a5%e7%ae%a1%e7%90%86)
    - [9.1.1. 短连接与长连接](#911-%e7%9f%ad%e8%bf%9e%e6%8e%a5%e4%b8%8e%e9%95%bf%e8%bf%9e%e6%8e%a5)
    - [9.1.2. 流水线](#912-%e6%b5%81%e6%b0%b4%e7%ba%bf)
  - [9.2. Cookie](#92-cookie)
    - [9.2.1. 用途](#921-%e7%94%a8%e9%80%94)
    - [cookie的获取,通过postman来模拟前端请求](#cookie%e7%9a%84%e8%8e%b7%e5%8f%96%e9%80%9a%e8%bf%87postman%e6%9d%a5%e6%a8%a1%e6%8b%9f%e5%89%8d%e7%ab%af%e8%af%b7%e6%b1%82)
    - [9.2.2. 创建过程](#922-%e5%88%9b%e5%bb%ba%e8%bf%87%e7%a8%8b)
    - [9.2.3. 分类](#923-%e5%88%86%e7%b1%bb)
    - [9.2.4. 作用域](#924-%e4%bd%9c%e7%94%a8%e5%9f%9f)
    - [9.2.5. JavaScript](#925-javascript)
    - [9.2.6. HttpOnly](#926-httponly)
    - [9.2.7. Secure](#927-secure)
    - [9.2.8. Session](#928-session)
    - [9.2.9. 浏览器禁用 Cookie](#929-%e6%b5%8f%e8%a7%88%e5%99%a8%e7%a6%81%e7%94%a8-cookie)
    - [9.2.10. Cookie 与 Session 选择](#9210-cookie-%e4%b8%8e-session-%e9%80%89%e6%8b%a9)
  - [9.3. 缓存](#93-%e7%bc%93%e5%ad%98)
    - [9.3.1. 优点](#931-%e4%bc%98%e7%82%b9)
    - [9.3.2. 实现方法](#932-%e5%ae%9e%e7%8e%b0%e6%96%b9%e6%b3%95)
    - [9.3.3. Cache-Control](#933-cache-control)
    - [9.3.4. 缓存验证](#934-%e7%bc%93%e5%ad%98%e9%aa%8c%e8%af%81)
  - [9.4. 内容协商](#94-%e5%86%85%e5%ae%b9%e5%8d%8f%e5%95%86)
    - [9.4.1. 类型](#941-%e7%b1%bb%e5%9e%8b)
    - [9.4.2. Vary](#942-vary)
  - [9.5. 内容编码](#95-%e5%86%85%e5%ae%b9%e7%bc%96%e7%a0%81)
  - [9.6. 范围请求](#96-%e8%8c%83%e5%9b%b4%e8%af%b7%e6%b1%82)
    - [9.6.1. Range](#961-range)
    - [9.6.2. Accept-Ranges](#962-accept-ranges)
    - [9.6.3. 响应状态码](#963-%e5%93%8d%e5%ba%94%e7%8a%b6%e6%80%81%e7%a0%81)
  - [9.7. 分块传输编码](#97-%e5%88%86%e5%9d%97%e4%bc%a0%e8%be%93%e7%bc%96%e7%a0%81)
  - [9.8. 多部分对象集合](#98-%e5%a4%9a%e9%83%a8%e5%88%86%e5%af%b9%e8%b1%a1%e9%9b%86%e5%90%88)
  - [9.9. 虚拟主机](#99-%e8%99%9a%e6%8b%9f%e4%b8%bb%e6%9c%ba)
  - [9.10. 通信数据转发](#910-%e9%80%9a%e4%bf%a1%e6%95%b0%e6%8d%ae%e8%bd%ac%e5%8f%91)
    - [9.10.1. 代理](#9101-%e4%bb%a3%e7%90%86)
    - [9.10.2. 网关](#9102-%e7%bd%91%e5%85%b3)
    - [9.10.3. 隧道](#9103-%e9%9a%a7%e9%81%93)
- [10. HTTPS](#10-https)
  - [10.1. 加密](#101-%e5%8a%a0%e5%af%86)
    - [10.1.1. 对称密钥加密](#1011-%e5%af%b9%e7%a7%b0%e5%af%86%e9%92%a5%e5%8a%a0%e5%af%86)
    - [10.1.2. 非对称密钥加密](#1012-%e9%9d%9e%e5%af%b9%e7%a7%b0%e5%af%86%e9%92%a5%e5%8a%a0%e5%af%86)
    - [10.1.3. HTTPS 采用的加密方式](#1013-https-%e9%87%87%e7%94%a8%e7%9a%84%e5%8a%a0%e5%af%86%e6%96%b9%e5%bc%8f)
  - [10.2. 认证](#102-%e8%ae%a4%e8%af%81)
  - [10.3. 完整性保护](#103-%e5%ae%8c%e6%95%b4%e6%80%a7%e4%bf%9d%e6%8a%a4)
  - [10.4. HTTPS 的缺点](#104-https-%e7%9a%84%e7%bc%ba%e7%82%b9)

<!-- /TOC -->

# 1. 一 、基础概念
## 1.1. 简介

HTTP协议是Hyper Text Transfer Protocol（超文本传输协议）的缩写,是用于从万维网（WWW:World Wide Web ）服务器传输超文本到本地浏览器的传送协议

HTTP是一个基于TCP/IP通信协议来传递数据（HTML 文件, 图片文件, 查询结果等）。

## 1.2. 工作原理
HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

Web服务器有：Apache服务器，IIS服务器（Internet Information Services）等。

Web服务器根据接收到的请求后，向客户端发送响应信息。

HTTP默认端口号为80，但是你也可以改为8080或者其他端口。

## 1.3. 特点
HTTP是无连接：无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求，并收到客户的应答后，即断开连接。采用这种方式可以节省传输时间。

HTTP是媒体独立的：这意味着，只要客户端和服务器知道如何处理的数据内容，任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。

HTTP是无状态：HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息，则它必须重传，这样可能导致每次连接传送的数据量增大。另一方面，在服务器不需要先前信息时它的应答就较快。

# 2. 知识结构(接下来几章将按照本章顺序详细介绍各个知识点)

## 2.1. 请求报文(分为请求行,请求头,请求体)

<div align="center"> <img src="pics/HTTP_RequestMessageExample.png" width=""/> </div><br>

### 2.1.1. 请求行(分为请求方法,URL,请求协议)
请求方法，HTTP/1.1 定义的请求方法有8种：GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE,最常的两种GET和POST，如果是RESTful接口的话一般会用到GET、POST、DELETE、PUT。

请求对应的URL地址，它和报文头的Host属性组成完整的请求URL

协议名称及版本号。

### 2.1.2. 请求头(重要属性,比如content-type)
HTTP的报文头，报文头包含若干个属性，格式为“属性名:属性值”，服务端据此获取客户端的信息。

其中一些重要的属性:
Content-Type 决定了请求体的格式,常用的有application/json

### 2.1.3. 请求体
报文体，它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串，它承载多个请求参数的数据。不但报文体可以传递请求参数，请求URL也可以通过类似于“/chapter15/user.html? param1=value1&param2=value2”的方式传递请求参数。

## 2.2. 响应报文

<div align="center"> <img src="pics/HTTP_ResponseMessageExample.png" width=""/> </div><br>

# 3. HTTP 方法

客户端发送的  **请求报文**  第一行为请求行，包含了方法字段。

## 3.1. GET

> 获取资源

当前网络请求中，绝大部分使用的是 GET 方法。

## 3.2. HEAD

> 获取报文首部

和 GET 方法类似，但是不返回报文实体主体部分。

主要用于确认 URL 的有效性以及资源更新的日期时间等。

## 3.3. POST

> 传输实体主体

POST 主要用来传输数据，而 GET 主要用来获取资源。

更多 POST 与 GET 的比较请见第九章。

## 3.4. PUT

> 上传文件

由于自身不带验证机制，任何人都可以上传文件，因此存在安全性问题，一般不使用该方法。

```html
PUT /new.html HTTP/1.1
Host: example.com
Content-type: text/html
Content-length: 16

New File

```

## 3.5. 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]
```

## 3.6. DELETE

> 删除文件

与 PUT 功能相反，并且同样不带验证机制。

```html
DELETE /file.html HTTP/1.1
```

## 3.7. OPTIONS

> 查询支持的方法

查询指定的 URL 能够支持的方法。

会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容。

## 3.8. CONNECT

> 要求在与代理服务器通信时建立隧道

使用 SSL（Secure Sockets Layer，安全套接层）和 TLS（Transport Layer Security，传输层安全）协议把通信内容加密后经网络隧道传输。

```html
CONNECT www.example.com:443 HTTP/1.1
```

<div align="center"> <img src="pics/dc00f70e-c5c8-4d20-baf1-2d70014a97e3.jpg" width=""/> </div><br>

## 3.9. TRACE

> 追踪路径

服务器会将通信路径返回给客户端。

发送请求时，在 Max-Forwards 首部字段中填入数值，每经过一个服务器就会减 1，当数值为 0 时就停止传输。

通常不会使用 TRACE，并且它容易受到 XST 攻击（Cross-Site Tracing，跨站追踪）。

# 4.GET 和 POST 比较

## 4.1. 作用

GET 用于获取资源，而 POST 用于传输实体主体。

## 4.2. 参数

GET 和 POST 的请求都能使用额外的参数，但是 GET 的参数是以查询字符串出现在 URL 中，而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高，因为照样可以通过一些抓包工具（Fiddler）查看。

因为 URL 只支持 ASCII 码，因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`，而空格会转换为 `%20`。POST 参数支持标准字符集。

```
GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
```

```
POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2
```

## 4.3. 安全

1. 安全的 HTTP 方法不会改变服务器状态，也就是说它只是可读的。

GET 方法是安全的，而 POST 却不是，因为 POST 的目的是传送实体主体内容，这个内容可能是用户上传的表单数据，上传成功之后，服务器可能把这个数据存储到数据库中，因此状态也就发生了改变。

安全的方法除了 GET 之外还有：HEAD、OPTIONS。

不安全的方法除了 POST 之外还有 PUT、DELETE。

2. GET比POST更不安全，因为参数直接暴露在URL上，所以不能用来传递敏感信息。

3. GET请求在URL中传送的参数是有长度限制的，而POST么有。

## 4.4. 幂等性

幂等的 HTTP 方法，同样的请求被执行一次与连续执行多次的效果是一样的，服务器的状态也是一样的。换句话说就是，幂等方法不应该具有副作用（统计用途除外）。

所有的安全方法也都是幂等的。

在正确实现的条件下，GET，HEAD，PUT 和 DELETE 等方法都是幂等的，而 POST 方法不是。

GET /pageX HTTP/1.1 是幂等的，连续调用多次，客户端接收到的结果都是一样的：

```
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
```

POST /add_row HTTP/1.1 不是幂等的，如果调用多次，就会增加多行记录：

```
POST /add_row HTTP/1.1   -> Adds a 1nd row
POST /add_row HTTP/1.1   -> Adds a 2nd row
POST /add_row HTTP/1.1   -> Adds a 3rd row
```

DELETE /idX/delete HTTP/1.1 是幂等的，即使不同的请求接收到的状态码不一样：

```
DELETE /idX/delete HTTP/1.1   -> Returns 200 if idX exists
DELETE /idX/delete HTTP/1.1   -> Returns 404 as it just got deleted
DELETE /idX/delete HTTP/1.1   -> Returns 404
```

## 4.5. 可缓存

如果要对响应进行缓存，需要满足以下条件：

- 请求报文的 HTTP 方法本身是可缓存的，包括 GET 和 HEAD，但是 PUT 和 DELETE 不可缓存，POST 在多数情况下不可缓存的,除非手动设置。
- 响应报文的状态码是可缓存的，包括：200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。

## 4.6. XMLHttpRequest

为了阐述 POST 和 GET 的另一个区别，需要先了解 XMLHttpRequest：

> XMLHttpRequest 是一个 API，它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式，并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。

- 在使用 XMLHttpRequest 的 POST 方法时，浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做，例如火狐就不会。
- 而 GET 方法 Header 和 Data 会一起发送。

## 4.7. TCP底层的区别

GET产生一个TCP数据包；POST产生两个TCP数据包。

对于GET方式的请求，浏览器会把http header和data一并发送出去，服务器响应200（返回数据）；

而对于POST，浏览器先发送header，服务器响应100 continue，浏览器再发送data，服务器响应200 ok（返回数据）。

# 5. URI

URI 包含 URL 和 URN。

URI 在于I(Identifier)是统一资源标示符，可以唯一标识一个资源。

URL在于Locater，一般来说（URL）统一资源定位符，可以提供找到该资源的路径，比如http://www.zhihu.com/question/21950864，但URL又是URI，因为它可以标识一个资源，所以URL又是URI的子集。举个是个URI但不是URL的例子：

urn:isbn:0-486-27557-4，这个是一本书的isbn，可以唯一标识这本书，更确切说这个是URN。

<div align="center"> <img src="pics/8441b2c4-dca7-4d6b-8efb-f22efccaf331.png" width="500px"> </div><br>

# HTTP/1.1 新特性

- **默认是长连接**
- 支持流水线
- 支持同时打开多个 TCP 连接
- 支持虚拟主机
- 新增状态码 100
- 支持分块传输编码
- 新增缓存处理指令 max-age

# 6. HTTP/2.0新特性

## 6.1. HTTP/1.x 缺陷

HTTP/1.x 实现简单是以牺牲性能为代价的：

- 客户端需要使用多个连接才能实现并发和缩短延迟；
- 不会压缩请求和响应首部，从而导致不必要的网络流量；
- 不支持有效的资源优先级，致使底层 TCP 连接的利用率低下。

## 6.2. 二进制/分帧层/底层tcp双向

HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧，它们都是二进制格式的。

<div align="center"> <img src="pics/86e6a91d-a285-447a-9345-c5484b8d0c47.png" width="400"/> </div><br>

在通信过程中，只会有一个 TCP 连接存在，它承载了任意数量的双向数据流（Stream）。

- 一个数据流（Stream）都有一个唯一标识符和可选的优先级信息，用于承载双向信息。
- 消息（Message）是与逻辑请求或响应对应的完整的一系列帧。
- 帧（Frame）是最小的通信单位，来自不同数据流的帧可以交错发送，然后再根据每个帧头的数据流标识符重新组装。

<div align="center"> <img src="pics/af198da1-2480-4043-b07f-a3b91a88b815.png" width="600"/> </div><br>

## 6.3. 服务端关联资源推送

HTTP/2.0 在客户端请求一个资源时，会把相关的资源一起发送给客户端，客户端就不需要再次发起请求了。例如客户端请求 page.html 页面，服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。

<div align="center"> <img src="pics/e3f1657c-80fc-4dfa-9643-bf51abd201c6.png" width="800"/> </div><br>

## 6.4. 请求头压缩

HTTP/1.1 的首部带有大量信息，而且每次都要重复发送。

HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表，从而避免了重复传输。

不仅如此，HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。

<div align="center"> <img src="pics/_u4E0B_u8F7D.png" width="600"/> </div><br>

# 7. HTTP 首部

有 4 种类型的首部字段：通用首部字段、请求首部字段、响应首部字段和实体首部字段。

各种首部字段及其含义如下（不需要全记，仅供查阅）：

## 7.1. 通用首部字段

| 首部字段名 | 说明 |
| :--: | :--: |
| Cache-Control | 控制缓存的行为 |
| Connection | 控制不再转发给代理的首部字段、管理持久连接|
| Date | 创建报文的日期时间 |
| Pragma | 报文指令 |
| Trailer | 报文末端的首部一览 |
| Transfer-Encoding | 指定报文主体的传输编码方式 |
| Upgrade | 升级为其他协议 |
| Via | 代理服务器的相关信息 |
| Warning | 错误通知 |

## 7.2. 请求首部字段

| 首部字段名 | 说明 |
| :--: | :--: |
| 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 客户端程序的信息 |

## 7.3. 响应首部字段

| 首部字段名 | 说明 |
| :--: | :--: |
| Accept-Ranges | 是否接受字节范围请求 |
| Age | 推算资源创建经过时间 |
| ETag | 资源的匹配信息 |
| Location | 令客户端重定向至指定 URI |
| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
| Retry-After | 对再次发起请求的时机要求 |
| Server | HTTP 服务器的安装信息 |
| Vary | 代理服务器缓存的管理信息 |
| WWW-Authenticate | 服务器对客户端的认证信息 |

## 7.4. 实体首部字段

| 首部字段名 | 说明 |
| :--: | :--: |
| Allow | 资源可支持的 HTTP 方法 |
| Content-Encoding | 实体主体适用的编码方式 |
| Content-Language | 实体主体的自然语言 |
| Content-Length | 实体主体的大小 |
| Content-Location | 替代对应资源的 URI |
| Content-MD5 | 实体主体的报文摘要 |
| Content-Range | 实体主体的位置范围 |
| Content-Type | 实体主体的媒体类型 |
| Expires | 实体主体过期的日期时间 |
| Last-Modified | 资源的最后修改日期时间 |

# 8. 响应状态码

服务器返回的  **响应报文**  中第一行为状态行，包含了状态码以及原因短语，用来告知客户端请求的结果。

| 状态码 | 类别 | 含义 |
| :---: | :---: | :---: |
| 1XX | Informational（信息性状态码） | 接收的请求正在处理 |
| 2XX | Success（成功状态码） | 请求正常处理完毕 |
| 3XX | Redirection（重定向状态码） | 需要进行附加操作以完成请求 |
| 4XX | Client Error（客户端错误状态码） | 服务器无法处理请求 |
| 5XX | Server Error（服务器错误状态码） | 服务器处理请求出错 |

## 8.1. XX 信息

-  **100 Continue** ：表明到目前为止都很正常，客户端可以继续发送请求或者忽略这个响应。

## 8.2. XX 成功

-  **200 OK**

-  **204 No Content** ：请求已经成功处理，但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息，而不需要返回数据时使用。

-  **206 Partial Content** ：表示客户端进行了范围请求，响应报文包含由 Content-Range 指定范围的实体内容。

## 8.3. XX 重定向

-  **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-Match，If-Modified-Since，If-None-Match，If-Range，If-Unmodified-Since，如果不满足条件，则服务器会返回 304 状态码。

-  **307 Temporary Redirect** ：临时重定向，与 302 的含义类似，但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。

## 8.4. XX 客户端错误

-  **400 Bad Request** ：请求报文中存在语法错误。

-  **401 Unauthorized** ：该状态码表示发送的请求需要有认证信息（BASIC 认证、DIGEST 认证）。如果之前已进行过一次请求，则表示用户认证失败。

-  **403 Forbidden** ：请求被拒绝。

-  **404 Not Found**

## 8.5. XX 服务器错误

-  **500 Internal Server Error** ：服务器正在执行请求时发生错误。

-  **503 Service Unavailable** ：服务器暂时处于超负载或正在进行停机维护，现在无法处理请求。

# 9. 具体应用

## 9.1. 连接管理

<div align="center"> <img src="pics/HTTP1_x_Connections.png" width="800"/> </div><br>

### 9.1.1. 短连接与长连接

当浏览器访问一个包含多张图片的 HTML 页面时，除了请求访问的 HTML 页面资源，还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接，那么开销会很大。

长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。

- 从 HTTP/1.1 开始默认是长连接的，如果要断开连接，需要由客户端或者服务器端提出断开，使用 `Connection : close`；
- 在 HTTP/1.1 之前默认是短连接的，如果需要使用长连接，则使用 `Connection : Keep-Alive`。

### 9.1.2. 流水线

默认情况下，HTTP 请求是按顺序发出的，下一个请求只有在当前请求收到响应之后才会被发出。由于受到网络延迟和带宽的限制，在下一个请求被发送到服务器之前，可能需要等待很长时间。

流水线是在同一条长连接上连续发出请求，而不用等待响应返回，这样可以减少延迟。

## 9.2. Cookie

HTTP 协议是无状态的，主要是为了让 HTTP 协议尽可能简单，使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据，它会在浏览器之后向同一服务器再次发起请求时被携带上，用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据，因此会带来额外的性能开销（尤其是在移动环境下）。

Cookie 曾一度用于客户端数据的存储，因为当时并没有其它合适的存储办法而作为唯一的存储手段，但现在随着现代浏览器开始支持各种各样的存储方式，Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地，如使用 Web storage API（本地存储和会话存储）或 IndexedDB。

### 9.2.1. 用途

- 会话状态管理（如用户登录状态、购物车、游戏分数或其它需要记录的信息）
- 个性化设置（如用户自定义设置、主题等）
- 浏览器行为跟踪（如跟踪分析用户行为等）

### cookie的获取,通过postman来模拟前端请求
从前端页面获取请求url:
f12  network  查看url
<div align="center"> <img src="./pictures/HTTP/Snipaste_2019-11-04_19-17-54.png" width="600"/> </div>

获取cookie:
f12  application   cookie

<div align="center"> <img src="./pictures/HTTP/Snipaste_2019-11-04_19-16-21.png" width="600"/> </div>

注意这个值是放在请求头里面的,所以不是放在postman的QueryParam,而是请求头

https://blog.csdn.net/qq_34178998/article/details/80361315

### 9.2.2. 创建过程

服务器发送的响应报文包含 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
```

### 9.2.3. 分类

- 会话期 Cookie：浏览器关闭之后它会被自动删除，也就是说它仅在会话期内有效。
- 持久性 Cookie：指定过期时间（Expires）或有效期（max-age）之后就成为了持久性的 Cookie。

```html
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
```

### 9.2.4. 作用域

Domain 标识指定了哪些主机可以接受 Cookie。如果不指定，默认为当前文档的主机（不包含子域名）。如果指定了 Domain，则一般包含子域名。例如，如果设置 Domain=mozilla.org，则 Cookie 也包含在子域名中（如 developer.mozilla.org）。

Path 标识指定了主机下的哪些路径可以接受 Cookie（该 URL 路径必须存在于请求 URL 中）。以字符 %x2F ("/") 作为路径分隔符，子路径也会被匹配。例如，设置 Path=/docs，则以下地址都会匹配：

- /docs
- /docs/Web/
- /docs/Web/HTTP

### 9.2.5. JavaScript

浏览器通过 `document.cookie` 属性可创建新的 Cookie，也可通过该属性访问非 HttpOnly 标记的 Cookie。

```html
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
```

### 9.2.6. HttpOnly

标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 `document.cookie` API 窃取用户的 Cookie 信息，因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。

```html
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
```

### 9.2.7. Secure

标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记，敏感信息也不应该通过 Cookie 传输，因为 Cookie 有其固有的不安全性，Secure 标记也无法提供确实的安全保障。

### 9.2.8. Session

除了可以将用户信息通过 Cookie 存储在用户浏览器中，也可以利用 Session 存储在服务器端，存储在服务器端的信息更加安全。

Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中，效率会更高。

使用 Session 维护用户登录状态的过程如下：

- 用户进行登录时，用户提交包含用户名和密码的表单，放入 HTTP 请求报文中；
- 服务器验证该用户名和密码，如果正确则把用户信息存储到 Redis 中，它在 Redis 中的 Key 称为 Session ID；
- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID，客户端收到响应报文之后将该 Cookie 值存入浏览器中；
- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值，服务器收到之后提取出 Session ID，从 Redis 中取出用户信息，继续之前的业务操作。

应该注意 Session ID 的安全性问题，不能让它被恶意攻击者轻易获取，那么就不能产生一个容易被猜到的 Session ID 值。此外，还需要经常重新生成 Session ID。在对安全性要求极高的场景下，例如转账等操作，除了使用 Session 管理用户状态之外，还需要对用户进行重新验证，比如重新输入密码，或者使用短信验证码等方式。

### 9.2.9. 浏览器禁用 Cookie

此时无法使用 Cookie 来保存用户信息，只能使用 Session。除此之外，不能再将 Session ID 存放到 Cookie 中，而是使用 URL 重写技术，将 Session ID 作为 URL 的参数进行传递。

### 9.2.10. Cookie 与 Session 选择

- Cookie 只能存储 ASCII 码字符串，而 Session 则可以存储任何类型的数据，因此在考虑数据复杂性时首选 Session；
- Cookie 存储在浏览器中，容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中，可以将 Cookie 值进行加密，然后在服务器进行解密；
- 对于大型网站，如果用户所有的信息都存储在 Session 中，那么开销是非常大的，因此不建议将所有的用户信息都存储到 Session 中。

## 9.3. 缓存

### 9.3.1. 优点

- 缓解服务器压力；
- 降低客户端获取资源的延迟：缓存通常位于内存中，读取缓存的速度更快。并且缓存服务器在地理位置上也有可能比源服务器来得近，例如浏览器缓存。

### 9.3.2. 实现方法

- 让代理服务器进行缓存；
- 让客户端浏览器进行缓存。

### 9.3.3. Cache-Control

HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。

**3.1 禁止进行缓存**

no-store 指令规定不能对请求或响应的任何一部分进行缓存。

```html
Cache-Control: no-store
```

**3.2 强制确认缓存**

no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性，只有当缓存资源有效时才能使用该缓存对客户端的请求进行响应。

```html
Cache-Control: no-cache
```

**3.3 私有缓存和公共缓存**

private 指令规定了将资源作为私有缓存，只能被单独用户使用，一般存储在用户浏览器中。

```html
Cache-Control: private
```

public 指令规定了将资源作为公共缓存，可以被多个用户使用，一般存储在代理服务器中。

```html
Cache-Control: public
```

**3.4 缓存过期机制**

max-age 指令出现在请求报文，并且缓存资源的缓存时间小于该指令指定的时间，那么就能接受该缓存。

max-age 指令出现在响应报文，表示缓存资源在缓存服务器中保存的时间。

```html
Cache-Control: max-age=31536000
```

Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。

```html
Expires: Wed, 04 Jul 2012 08:26:05 GMT
```

- 在 HTTP/1.1 中，会优先处理 max-age 指令；
- 在 HTTP/1.0 中，max-age 指令会被忽略掉。

### 9.3.4. 缓存验证

需要先了解 ETag 首部字段的含义，它是资源的唯一标识。URL 不能唯一表示资源，例如 `http://www.google.com/` 有中文和英文两个资源，只有 ETag 才能对这两个资源进行唯一标识。

```html
ETag: "82e22293907ce725faf67773957acd12"
```

可以将缓存资源的 ETag 值放入 If-None-Match 首部，服务器收到该请求后，判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致，如果一致则表示缓存资源有效，返回 304 Not Modified。

```html
If-None-Match: "82e22293907ce725faf67773957acd12"
```

Last-Modified 首部字段也可以用于缓存验证，它包含在源服务器发送的响应报文中，指示源服务器对资源的最后修改时间。但是它是一种弱校验器，因为只能精确到一秒，所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息，客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回，状态码为 200 OK。如果请求的资源从那时起未经修改，那么返回一个不带有实体主体的 304 Not Modified 响应报文。

```html
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
```

```html
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
```

## 9.4. 内容协商

通过内容协商返回最合适的内容，例如根据浏览器的默认语言选择返回中文界面还是英文界面。

### 9.4.1. 类型

**1.1 服务端驱动型**

客户端设置特定的 HTTP 首部字段，例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language，服务器根据这些字段返回特定的资源。

它存在以下问题：

- 服务器很难知道客户端浏览器的全部信息；
- 客户端提供的信息相当冗长（HTTP/2 协议的首部压缩机制缓解了这个问题），并且存在隐私风险（HTTP 指纹识别技术）；
- 给定的资源需要返回不同的展现形式，共享缓存的效率会降低，而服务器端的实现会越来越复杂。

**1.2 代理驱动型**

服务器返回 300 Multiple Choices 或者 406 Not Acceptable，客户端从中选出最合适的那个资源。

### 9.4.2. Vary

```html
Vary: Accept-Language
```

在使用内容协商的情况下，只有当缓存服务器中的缓存满足内容协商条件时，才能使用该缓存，否则应该向源服务器请求该资源。

例如，一个客户端发送了一个包含 Accept-Language 首部字段的请求之后，源服务器返回的响应包含 `Vary: Accept-Language` 内容，缓存服务器对这个响应进行缓存之后，在客户端下一次访问同一个 URL 资源，并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。

## 9.5. 内容编码

内容编码将实体主体进行压缩，从而减少传输的数据量。

常用的内容编码有：gzip、compress、deflate、identity。

浏览器发送 Accept-Encoding 首部，其中包含有它所支持的压缩算法，以及各自的优先级。服务器则从中选择一种，使用该算法对响应的消息主体进行压缩，并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的，响应报文的 Vary 首部字段至少要包含 Content-Encoding。

## 9.6. 范围请求

如果网络出现中断，服务器只发送了一部分数据，范围请求可以使得客户端只请求服务器未发送的那部分数据，从而避免服务器重新发送所有数据。

### 9.6.1. Range

在请求报文中添加 Range 首部字段指定请求的范围。

```html
GET /z4d4kWk.jpg HTTP/1.1
Host: i.imgur.com
Range: bytes=0-1023
```

请求成功的话服务器返回的响应包含 206 Partial Content 状态码。

```html
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
...
(binary content)
```

### 9.6.2. Accept-Ranges

响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求，可以处理使用 bytes，否则使用 none。

```html
Accept-Ranges: bytes
```

### 9.6.3. 响应状态码

- 在请求成功的情况下，服务器会返回 206 Partial Content 状态码。
- 在请求的范围越界的情况下，服务器会返回 416 Requested Range Not Satisfiable 状态码。
- 在不支持范围请求的情况下，服务器会返回 200 OK 状态码。

## 9.7. 分块传输编码

Chunked Transfer Encoding，可以把数据分割成多块，让浏览器逐步显示页面。

## 9.8. 多部分对象集合

一份报文主体内可含有多种类型的实体同时发送，每个部分之间用 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--
```

## 9.9. 虚拟主机

HTTP/1.1 使用虚拟主机技术，使得一台服务器拥有多个域名，并且在逻辑上可以看成多个服务器。

## 9.10. 通信数据转发

### 9.10.1. 代理

代理服务器接受客户端的请求，并且转发给其它服务器。

使用代理的主要目的是：

- 缓存
- 负载均衡
- 网络访问控制
- 访问日志记录

代理服务器分为正向代理和反向代理两种：

- 用户察觉得到正向代理的存在。

<div align="center"> <img src="pics/a314bb79-5b18-4e63-a976-3448bffa6f1b.png" width=""/> </div><br>

- 而反向代理一般位于内部网络中，用户察觉不到。

<div align="center"> <img src="pics/2d09a847-b854-439c-9198-b29c65810944.png" width=""/> </div><br>

### 9.10.2. 网关

与代理服务器不同的是，网关服务器会将 HTTP 转化为其它协议进行通信，从而请求其它非 HTTP 服务器的服务。

### 9.10.3. 隧道

使用 SSL 等加密手段，在客户端和服务器之间建立一条安全的通信线路。

# 10. HTTPS

HTTP 有以下安全性问题：

- 使用明文进行通信，内容可能会被窃听；
- 不验证通信方的身份，通信方的身份有可能遭遇伪装；
- 无法证明报文的完整性，报文有可能遭篡改。

HTTPS 并不是新协议，而是让 HTTP 先和 SSL（Secure Sockets Layer）通信，再由 SSL 和 TCP 通信，也就是说 HTTPS 使用了隧道进行通信。

通过使用 SSL，HTTPS 具有了加密（防窃听）、认证（防伪装）和完整性保护（防篡改）。

<div align="center"> <img src="pics/ssl-offloading.jpg" width="700"/> </div><br>

## HTTP利用SSL通讯的过程

#### SSL的位置

SSL介于应用层和TCP层之间。应用层数据不再直接传递给传输层，而是传递给SSL层，SSL层对从应用层收到的数据进行加密，并增加自己的SSL头。

RSA性能是非常低的，原因在于寻找大素数、大数计算、数据分割需要耗费很多的CPU周期，所以一般的HTTPS连接只在第一次握手时使用非对称加密，通过握手交换对称加密密钥，在之后的通信走对称加密。

### HTTPS 中的 SSL 握手建立过程

假设现在有客户端 A 和服务器 B ：

1. 首先，客户端 A 访问服务器 B ，比如我们用浏览器打开一个网页 [www.baidu.com](https://www.baidu.com/) ，这时，浏览器就是客户端 A ，百度的服务器就是服务器 B 了。这时候客户端 A 会生成一个随机数1，把随机数1 、自己支持的 SSL 版本号以及加密算法等这些信息告诉服务器 B 。
2. 服务器 B 知道这些信息后，然后确认一下双方的加密算法，然后服务端也生成一个随机数 B ，并将随机数 B 和 CA 颁发给自己的证书一同返回给客户端 A 。
3. 客户端 A 得到 CA 证书后，会去校验该 CA 证书的有效性，校验方法在上面已经说过了。校验通过后，客户端生成一个随机数3 ，然后用证书中的公钥加密随机数3 并传输给服务端 B 。
4. 服务端 B 得到加密后的随机数3，然后利用私钥进行解密，得到真正的随机数3。
5. 最后，客户端 A 和服务端 B 都有随机数1、随机数2、随机数3，然后双方利用这三个随机数生成一个对话密钥。之后传输内容就是利用对话密钥来进行加解密了。这时就是利用了对称加密，一般用的都是 AES 算法。
6. 客户端 A 通知服务端 B ，指明后面的通讯用对话密钥来完成，同时通知服务器 B 客户端 A 的握手过程结束。
7. 服务端 B 通知客户端 A，指明后面的通讯用对话密钥来完成，同时通知客户端 A 服务器 B 的握手过程结束。
8. SSL 的握手部分结束，SSL 安全通道的数据通讯开始，客户端 A 和服务器 B 开始使用相同的对话密钥进行数据通讯。

到此，SSL 握手过程就讲完了。可能上面的流程太过于复杂，我们简单地来讲：

1. 客户端和服务端建立 SSL 握手，客户端通过 CA 证书来确认服务端的身份；
2. 互相传递三个随机数，之后通过这随机数来生成一个密钥；
3. 互相确认密钥，然后握手结束；
4. 数据通讯开始，都使用同一个对话密钥来加解密；

我们可以发现，在 HTTPS 加密原理的过程中把对称加密和非对称加密都利用了起来。即利用了非对称加密安全性高的特点，又利用了对称加密速度快，效率高的好处。真的是设计得非常精妙，令人赞不绝口。

## 10.1. 加密

### 10.1.1. 对称密钥加密

对称密钥加密（Symmetric-Key Encryption），加密和解密使用同一密钥。

- 优点：运算速度快；
- 缺点：无法安全地将密钥传输给通信方。

<div align="center"> <img src="pics/7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png" width="600"/> </div><br>

### 10.1.2. 非对称密钥加密

非对称密钥加密，又称公开密钥加密（Public-Key Encryption），加密和解密使用不同的密钥。

公开密钥所有人都可以获得，通信发送方获得接收方的公开密钥之后，就可以使用公开密钥进行加密，接收方收到通信内容后使用私有密钥解密。

非对称密钥除了用来加密，还可以用来进行签名。因为私有密钥无法被其他人获取，因此通信发送方使用其私有密钥进行签名，通信接收方使用发送方的公开密钥对签名进行解密，就能判断这个签名是否正确。

- 优点：可以更安全地将公开密钥传输给通信发送方；
- 缺点：运算速度慢。

<div align="center"> <img src="pics/39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png" width="600"/> </div><br>

### 10.1.3. HTTPS 采用的加密方式

HTTPS 采用混合的加密机制，使用**非对称密钥加密**用于**传输对称密钥**,来保证传输过程的安全性，之后使用对称密钥加密进行通信来保证通信过程的效率。（下图中的 Session Key 就是对称密钥）

<div align="center"> <img src="pics/How-HTTPS-Works.png" width="600"/> </div><br>

## 10.2. 认证

通过使用  **证书**  来对通信方进行认证。

数字证书认证机构（CA，Certificate Authority）是客户端与服务器双方都可信赖的第三方机构。

服务器的运营人员向 CA 提出公开密钥的申请，CA 在判明提出申请者的身份之后，会对已申请的公开密钥做数字签名，然后分配这个已签名的公开密钥，并将该公开密钥放入公开密钥证书后绑定在一起。

进行 HTTPS 通信时，服务器会把证书发送给客户端。客户端取得其中的公开密钥之后，先使用数字签名进行验证，如果验证通过，就可以开始通信了。

<div align="center"> <img src="pics/2017-06-11-ca.png" width=""/> </div><br>

## 10.3. 完整性保护

SSL 提供报文摘要功能来进行完整性保护。

HTTP 也提供了 MD5 报文摘要功能，但不是安全的。例如报文内容被篡改之后，同时重新计算 MD5 的值，通信接收方是无法意识到发生了篡改。

HTTPS 的报文摘要功能之所以安全，是因为它结合了加密和认证这两个操作。试想一下，加密之后的报文，遭到篡改之后，也很难重新计算报文摘要，因为无法轻易获取明文。

## 10.4. HTTPS 的缺点

- 因为需要进行加密解密等过程，因此速度会更慢；
- 需要支付证书授权的高额费用。
<file_sep>/notes/Socket.md
<!-- TOC -->

- [一、I/O 模型](#%e4%b8%80io-%e6%a8%a1%e5%9e%8b)
  - [阻塞式 I/O](#%e9%98%bb%e5%a1%9e%e5%bc%8f-io)
  - [非阻塞式 I/O](#%e9%9d%9e%e9%98%bb%e5%a1%9e%e5%bc%8f-io)
  - [I/O 复用](#io-%e5%a4%8d%e7%94%a8)
  - [信号驱动 I/O](#%e4%bf%a1%e5%8f%b7%e9%a9%b1%e5%8a%a8-io)
  - [异步 I/O](#%e5%bc%82%e6%ad%a5-io)
  - [五大 I/O 模型比较](#%e4%ba%94%e5%a4%a7-io-%e6%a8%a1%e5%9e%8b%e6%af%94%e8%be%83)
- [二、I/O 复用](#%e4%ba%8cio-%e5%a4%8d%e7%94%a8)
  - [select](#select)
  - [poll](#poll)
  - [比较](#%e6%af%94%e8%be%83)
    - [1. 功能](#1-%e5%8a%9f%e8%83%bd)
    - [2. 速度](#2-%e9%80%9f%e5%ba%a6)
    - [3. 可移植性](#3-%e5%8f%af%e7%a7%bb%e6%a4%8d%e6%80%a7)
  - [epoll](#epoll)
  - [工作模式](#%e5%b7%a5%e4%bd%9c%e6%a8%a1%e5%bc%8f)
    - [1. LT 模式](#1-lt-%e6%a8%a1%e5%bc%8f)
    - [2. ET 模式](#2-et-%e6%a8%a1%e5%bc%8f)
  - [应用场景](#%e5%ba%94%e7%94%a8%e5%9c%ba%e6%99%af)
    - [1. select 应用场景](#1-select-%e5%ba%94%e7%94%a8%e5%9c%ba%e6%99%af)
    - [2. poll 应用场景](#2-poll-%e5%ba%94%e7%94%a8%e5%9c%ba%e6%99%af)
    - [3. epoll 应用场景](#3-epoll-%e5%ba%94%e7%94%a8%e5%9c%ba%e6%99%af)
- [Linux IO模式及 select、poll、epoll详解](#linux-io%e6%a8%a1%e5%bc%8f%e5%8f%8a-selectpollepoll%e8%af%a6%e8%a7%a3)
- [一 概念说明](#%e4%b8%80-%e6%a6%82%e5%bf%b5%e8%af%b4%e6%98%8e)
  - [用户空间与内核空间](#%e7%94%a8%e6%88%b7%e7%a9%ba%e9%97%b4%e4%b8%8e%e5%86%85%e6%a0%b8%e7%a9%ba%e9%97%b4)
  - [进程切换](#%e8%bf%9b%e7%a8%8b%e5%88%87%e6%8d%a2)
  - [进程的阻塞](#%e8%bf%9b%e7%a8%8b%e7%9a%84%e9%98%bb%e5%a1%9e)
  - [文件描述符fd](#%e6%96%87%e4%bb%b6%e6%8f%8f%e8%bf%b0%e7%ac%a6fd)
  - [缓存 I/O](#%e7%bc%93%e5%ad%98-io)
- [二 IO模式](#%e4%ba%8c-io%e6%a8%a1%e5%bc%8f)
  - [阻塞 I/O（blocking IO）](#%e9%98%bb%e5%a1%9e-ioblocking-io)
  - [非阻塞 I/O（nonblocking IO）](#%e9%9d%9e%e9%98%bb%e5%a1%9e-iononblocking-io)
  - [I/O 多路复用（ IO multiplexing）](#io-%e5%a4%9a%e8%b7%af%e5%a4%8d%e7%94%a8-io-multiplexing)
  - [异步 I/O（asynchronous IO）](#%e5%bc%82%e6%ad%a5-ioasynchronous-io)
  - [总结](#%e6%80%bb%e7%bb%93)
    - [blocking和non-blocking的区别](#blocking%e5%92%8cnon-blocking%e7%9a%84%e5%8c%ba%e5%88%ab)
    - [synchronous IO和asynchronous IO的区别](#synchronous-io%e5%92%8casynchronous-io%e7%9a%84%e5%8c%ba%e5%88%ab)
- [三 I/O 多路复用之select、poll、epoll详解](#%e4%b8%89-io-%e5%a4%9a%e8%b7%af%e5%a4%8d%e7%94%a8%e4%b9%8bselectpollepoll%e8%af%a6%e8%a7%a3)
  - [select(1024限制,返回后遍历read,write,except位图)](#select1024%e9%99%90%e5%88%b6%e8%bf%94%e5%9b%9e%e5%90%8e%e9%81%8d%e5%8e%86readwriteexcept%e4%bd%8d%e5%9b%be)
  - [poll(无限制,不使用位图,使用pollfd描述符)](#poll%e6%97%a0%e9%99%90%e5%88%b6%e4%b8%8d%e4%bd%bf%e7%94%a8%e4%bd%8d%e5%9b%be%e4%bd%bf%e7%94%a8pollfd%e6%8f%8f%e8%bf%b0%e7%ac%a6)
  - [epoll](#epoll-1)
    - [零拷贝技术](#%e9%9b%b6%e6%8b%b7%e8%b4%9d%e6%8a%80%e6%9c%af)
    - [一 epoll操作过程](#%e4%b8%80-epoll%e6%93%8d%e4%bd%9c%e8%bf%87%e7%a8%8b)
    - [二 工作模式](#%e4%ba%8c-%e5%b7%a5%e4%bd%9c%e6%a8%a1%e5%bc%8f)
      - [1. LT模式](#1-lt%e6%a8%a1%e5%bc%8f)
      - [2. ET模式](#2-et%e6%a8%a1%e5%bc%8f)
      - [3. 总结](#3-%e6%80%bb%e7%bb%93)
    - [三 代码演示](#%e4%b8%89-%e4%bb%a3%e7%a0%81%e6%bc%94%e7%a4%ba)
    - [四 epoll总结](#%e5%9b%9b-epoll%e6%80%bb%e7%bb%93)
- [epoll的水平触发与边沿触发](#epoll%e7%9a%84%e6%b0%b4%e5%b9%b3%e8%a7%a6%e5%8f%91%e4%b8%8e%e8%be%b9%e6%b2%bf%e8%a7%a6%e5%8f%91)

<!-- /TOC -->

# 一、I/O 模型

一个输入操作通常包括两个阶段：

- 等待数据准备好
- 从内核向进程复制数据

对于一个套接字上的输入操作，第一步通常涉及等待数据从网络中到达。当所等待数据到达时，它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

Unix 有五种 I/O 模型：

- 阻塞式 I/O
- 非阻塞式 I/O
- I/O 复用（select 和 poll）
- 信号驱动式 I/O（SIGIO）
- 异步 I/O（AIO）

## 阻塞式 I/O

应用进程被阻塞，直到数据从内核缓冲区复制到应用进程缓冲区中才返回。

应该注意到，在阻塞的过程中，其它应用进程还可以执行，因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行，所以不消耗 CPU 时间，这种模型的 CPU 利用率会比较高。

下图中，recvfrom() 用于接收 Socket 传来的数据，并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。

```c
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
```

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492928416812_4.png"/> </div><br>

## 非阻塞式 I/O

应用进程执行系统调用之后，内核返回一个错误码。应用进程可以继续执行，但是需要不断的执行系统调用来获知 I/O 是否完成，这种方式称为轮询（polling）。

由于 CPU 要处理更多的系统调用，因此这种模型的 CPU 利用率比较低。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492929000361_5.png"/> </div><br>

## I/O 复用

使用 select 或者 poll 等待数据，并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞，当某一个套接字可读时返回，之后再使用 recvfrom 把数据从内核复制到进程中。

它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O，即事件驱动 I/O。

如果一个 Web 服务器没有 I/O 复用，那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接，那么就需要创建相同数量的线程。相比于多进程和多线程技术，I/O 复用不需要进程线程创建和切换的开销，系统开销更小。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492929444818_6.png"/> </div><br>

## 信号驱动 I/O

应用进程使用 sigaction 系统调用，内核立即返回，应用进程可以继续执行，也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号，应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。

相比于非阻塞式 I/O 的轮询方式，信号驱动 I/O 的 CPU 利用率更高。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492929553651_7.png"/> </div><br>

## 异步 I/O

应用进程执行 aio_read 系统调用会立即返回，应用进程可以继续执行，不会被阻塞，内核会在所有操作完成之后向应用进程发送信号。

异步 I/O 与信号驱动 I/O 的区别在于，异步 I/O 的信号是通知应用进程 I/O 完成，而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492930243286_8.png"/> </div><br>

## 五大 I/O 模型比较

- 同步 I/O：将数据从内核缓冲区复制到应用进程缓冲区的阶段（第二阶段），应用进程会阻塞。
- 异步 I/O：第二阶段应用进程不会阻塞。

同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ，它们的主要区别在第一个阶段。

非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492928105791_3.png"/> </div><br>

# 二、I/O 复用

select/poll/epoll 都是 I/O 多路复用的具体实现，select 出现的最早，之后是 poll，再是 epoll。

## select

```c
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
```

**select 允许应用程序监视一组文件描述符，等待一个或者多个描述符成为就绪状态，从而完成 I/O 操作。**

- fd_set 使用数组实现，数组大小使用 FD_SETSIZE 定义，所以只能监听少于 FD_SETSIZE 数量的描述符。有三种类型的描述符类型：readset、writeset、exceptset，分别对应读、写、异常条件的描述符集合
- fd_in和fd_out是两个文件描述符数组.一旦有就绪,就让select返回
- timeout 为超时参数，调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。
- 成功调用返回结果大于 0，出错返回结果为 -1，超时返回结果为 0。

```c
fd_set fd_in, fd_out;
struct timeval tv;

// Reset the sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );

// Monitor sock1 for input events
FD_SET( sock1, &fd_in );

// Monitor sock2 for output events
FD_SET( sock2, &fd_out );

// Find out which socket has the largest numeric value as select requires it
int largest_sock = sock1 > sock2 ? sock1 : sock2;

// Wait up to 10 seconds
tv.tv_sec = 10;
tv.tv_usec = 0;

// Call the select
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );

// Check if select actually succeed
if ( ret == -1 )
    // report error and abort
else if ( ret == 0 )
    // timeout; no event detected
else
{
    if ( FD_ISSET( sock1, &fd_in ) )
        // input event on sock1

    if ( FD_ISSET( sock2, &fd_out ) )
        // output event on sock2
}
```

多路复用IO是一种同步IO,

第一步进程发送读操作,进程阻塞.

第二步用select或者poll或者epoll在内核读取数据,当读取到数据之后,

第三步遍历文件描述符数组/链表, 把有数据的文件描述符的数据,从内核的数据拷贝到用户进程内存

<img src="pictures/Socket/image-20200418112823764.png" alt="image-20200418112823764" style="zoom:50%;" />

<img src="pictures/Socket/image-20200418091521057.png" alt="image-20200418091521057" style="zoom:50%;" />

（1）使用copy_from_user从用户空间拷贝fd_set到内核空间

（2）注册回调函数__pollwait

（3）遍历所有fd，调用其对应的poll方法（对于socket，这个poll方法是sock_poll，sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll）

（4）以tcp_poll为例，其核心实现就是__pollwait，也就是上面注册的回调函数。

（5）__pollwait的主要工作就是把current（当前进程）挂到设备的等待队列中，不同的设备有不同的等待队列，对于tcp_poll来说，其等待队列是sk->sk_sleep（注意把进程挂到等待队列中并不代表进程已经睡眠了）。在设备收到一条消息（网络设备）或填写完文件数据（磁盘设备）后，会唤醒设备等待队列上睡眠的进程，这时current便被唤醒了。

（6）poll方法返回时会返回一个描述读写操作是否就绪的mask掩码，根据这个mask掩码给fd_set赋值。

（7）如果遍历完所有的fd，还没有返回一个可读写的mask掩码，则会调用schedule_timeout是调用select的进程（也就是current）进入睡眠。当设备驱动发生自身资源可读写后，会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间（schedule_timeout指定），还是没人唤醒，则调用select的进程会重新被唤醒获得CPU，进而重新遍历fd，判断有没有就绪的fd。

（8）把fd_set从内核空间拷贝到用户空间。

**总结：**

**select的几大缺点：**

**（1）每次调用select，都需要把fd集合从用户态拷贝到内核态，这个开销在fd很多时会很大**

**（2）同时每次调用select都需要在内核遍历传递进来的所有fd，这个开销在fd很多时也很大**

**（3）select支持的文件描述符数量太小了，默认是1024**

## poll

```c
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
```

poll 的功能与 select 类似，也是**等待一组描述符中的一个成为就绪状态。**

**fds指向的是一个pollfd动态数组**

**revents和 POLL_IN,POLL_OUT相&&,**决定输入输出
**events里面存放数据**

poll 中的描述符是 pollfd 类型的数组，pollfd 的定义如下：

```c
struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

```

```c
// The structure for two events
struct pollfd fds[2];

// Monitor sock1 for input
fds[0].fd = sock1;
fds[0].events = POLLIN;

// Monitor sock2 for output
fds[1].fd = sock2;
fds[1].events = POLLOUT;

// Wait 10 seconds
int ret = poll( &fds, 2, 10000 );
// Check if poll actually succeed
if ( ret == -1 )
    // report error and abort
else if ( ret == 0 )
    // timeout; no event detected
else
{
    // If we detect the event, zero it out so we can reuse the structure
    if ( fds[0].revents & POLLIN )
        fds[0].revents = 0;
        // input event on sock1

    if ( fds[1].revents & POLLOUT )
        fds[1].revents = 0;
        // output event on sock2
}
```

## 比较

### 1. 功能

select 和 poll 的功能基本相同，不过在一些实现细节上有所不同。

- select 会修改描述符，而 poll 不会；
- select 的描述符类型使用数组实现，FD_SETSIZE 大小默认为 1024，因此默认只能监听 1024 个描述符。如果要监听更多描述符的话，需要修改 FD_SETSIZE 之后重新编译；而 poll 没有描述符数量的限制；
- poll 提供了更多的事件类型，并且对描述符的重复利用上比 select 高。
- 如果一个线程对某个描述符调用了 select 或者 poll，另一个线程关闭了该描述符，会导致调用结果不确定。

### 2. 速度

**select 和 poll 速度都比较慢，**

**每次都会遍历  整个位图或者整个的poll_fd链表.**

**包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间**，而不论这些文件描述符是否就绪，它的开销随着文件描述符数量的增加而线性增大

## epoll

```c
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)；
//等待epfd上的io事件，最多返回maxevents个事件。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epfd 是create的返回值,当调用epoll_create之后,返回epoll句柄后，它就会占用一个fd值，在linux下如果查看/proc/进程id/fd/，是能够看到这个fd的，所以在使用完epoll后，必须调用close()关闭，否则可能导致fd被耗尽。

fd   是要监听的文件描述符,把它放到epfd上

epfd存在于内核上,当有数据进入epfd的时候,也就是进入了events,wait会返回events里面的数据量

struct epoll_event {
  __uint32_t events;  /* Epoll events */决定是输入还是输出
  epoll_data_t data;  /* User data variable */存储着数据
};

```

epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上，通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理，进程调用 epoll_wait() 便可以得到事件完成的描述符。

从上面的描述可以看出，**epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次**，并且**进程不需要通过轮询来获得事件完成的描述符。**

epoll 仅适用于 Linux OS。

epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。

epoll 对多线程编程更有友好，一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。

```c
// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
// The function argument is ignored (it was not before, but now it is), so put your favorite number here
int pollingfd = epoll_create( 0xCAFE );

if ( pollingfd < 0 )
 // report error

// Initialize the epoll structure in case more members are added in future
struct epoll_event ev = { 0 };

// Associate the connection class instance with the event. You can associate anything
// you want, epoll does not use this information. We store a connection class pointer, pConnection1
ev.data.ptr = pConnection1;

// Monitor for input, and do not automatically rearm the descriptor after the event
ev.events = EPOLLIN | EPOLLONESHOT;
// Add the descriptor into the monitoring list. We can do it even if another thread is
// waiting in epoll_wait - the descriptor will be properly added
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
    // report error

// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)
struct epoll_event pevents[ 20 ];

// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array
int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
// Check if epoll actually succeed
if ( ret == -1 )
    // report error and abort
else if ( ret == 0 )
    // timeout; no event detected
else
{
    // Check if any events detected
    for ( int i = 0; i < ret; i++ )
    {
        if ( pevents[i].events & EPOLLIN )
        {
            // Get back our connection pointer
            Connection * c = (Connection*) pevents[i].data.ptr;
            c->handleReadEvent();
         }
    }
}
```

## 工作模式

epoll 的描述符事件有两种触发模式：LT（level trigger）和 ET（edge trigger）。

### 1. LT 模式

当 epoll_wait() 检测到描述符事件到达时，将此事件通知进程，进程可以不立即处理该事件，下次调用 epoll_wait() 会再次通知进程。是默认的一种模式，并且同时支持 Blocking 和 No-Blocking。

### 2. ET 模式

和 LT 模式不同的是，通知之后进程必须立即处理事件，下次再调用 epoll_wait() 时不会再得到事件到达的通知。

很大程度上减少了 epoll 事件被重复触发的次数，因此效率要比 LT 模式高。只支持 No-Blocking，以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

### epoll为什么要有EPOLLET触发模式？

如果采用EPOLLLT模式的话，**系统中一旦有大量你不需要读写的就绪文件描述符，它们每次调用epoll_wait都会返回**，这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话，当被监控的文件描述符上有可读写事件发生时，epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小)，那么下次调用epoll_wait()时，它不会通知你，也就是它只会通知你一次，直到该文件描述符上出现第二次可读写事件才会通知你！！！**这种模式比水平触发效率高，系统不会充斥大量你不关心的就绪文件描述符**

## 应用场景

很容易产生一种错觉认为只要用 epoll 就可以了，select 和 poll 都已经过时了，其实它们都有各自的使用场景。

**在连接数少并且连接都十分活跃的情况下，select和poll的性能可能比epoll好，毕竟epoll的通知机制需要很多函数回调。**

# Linux IO模式及 select、poll、epoll详解

# 一 概念说明

在进行解释之前，首先要说明几个概念：
\- 用户空间和内核空间
\- 进程切换
\- 进程的阻塞
\- 文件描述符
\- 缓存 I/O

## 用户空间与内核空间

现在操作系统都是采用虚拟存储器，那么对32位操作系统而言，它的寻址空间（虚拟存储空间）为4G（2的32次方）。操作系统的核心是内核，独立于普通的应用程序，可以访问受保护的内存空间，也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核（kernel），保证内核的安全，操心系统将虚拟空间划分为两部分，一部分为内核空间，一部分为用户空间。针对linux操作系统而言，将最高的1G字节（从虚拟地址0xC0000000到0xFFFFFFFF），供内核使用，称为内核空间，而将较低的3G字节（从虚拟地址0x00000000到0xBFFFFFFF），供各个进程使用，称为用户空间。

## 进程切换

为了控制进程的执行，内核必须有能力挂起正在CPU上运行的进程，并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说，任何进程都是在操作系统内核的支持下运行的，是与内核紧密相关的。

从一个进程的运行转到另一个进程上运行，这个过程中经过下面这些变化：
\1. 保存处理机上下文，包括程序计数器和其他寄存器。
\2. 更新PCB信息。
\3. 把进程的PCB移入相应的队列，如就绪、在某事件阻塞等队列。
\4. 选择另一个进程执行，并更新其PCB。
\5. 更新内存管理的数据结构。
\6. 恢复处理机上下文。

注：**总而言之就是很耗资源**，具体的可以参考这篇文章：[进程切换](http://guojing.me/linux-kernel-architecture/posts/process-switch/)

## 进程的阻塞

正在执行的进程，由于期待的某些事件未发生，如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等，则由系统自动执行阻塞原语(Block)，使自己由运行状态变为阻塞状态。可见，进程的阻塞是进程自身的一种主动行为，也因此只有处于运行态的进程（获得CPU），才可能将其转为阻塞状态。`当进程进入阻塞状态，是不占用CPU资源的`。

## 文件描述符fd

文件描述符（File descriptor）是计算机科学中的一个术语，是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上，**它是一个索引值，指向内核为每一个进程所维护的该进程打开文件的记录表。**当程序打开一个现有文件或者创建一个新文件时，内核向进程返回一个文件描述符。在程序设计中，一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

## 缓存 I/O

缓存 I/O 又被称作标准 I/O，大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中，操作系统会将 I/O 的数据缓存在文件系统的页缓存（ page cache ）中，也就是说，**数据会先被拷贝到操作系统内核的缓冲区中，然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。**

**缓存 I/O 的缺点：**
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作，这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

# 二 IO模式

刚才说了，对于一次IO访问（以read举例），数据会先被拷贝到操作系统内核的缓冲区中，然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说，当一个read操作发生时，它会经历两个阶段：
\1. 等待数据准备 (Waiting for the data to be ready)
\2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式因为这两个阶段，linux系统产生了下面五种网络模式的方案。
\- 阻塞 I/O（blocking IO）
\- 非阻塞 I/O（nonblocking IO）
\- I/O 多路复用（ IO multiplexing）
\- 信号驱动 I/O（ signal driven IO）
\- 异步 I/O（asynchronous IO）

注：由于signal driven IO在实际中并不常用，所以我这只提及剩下的四种IO Model。

## 阻塞 I/O（blocking IO）

在linux中，默认情况下所有的socket都是blocking，一个典型的读操作流程大概是这样：
![clipboard.png](pictures/Socket/bVm1c3.png)

当用户进程调用了recvfrom这个系统调用，kernel就开始了IO的第一个阶段：准备数据（对于网络IO来说，很多时候数据在一开始还没有到达。比如，还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来）。这个过程需要等待，也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边，整个进程会被阻塞（当然，是进程自己选择的阻塞）。当kernel一直等到数据准备好了，它就会将数据从kernel中拷贝到用户内存，然后kernel返回结果，用户进程才解除block的状态，重新运行起来。

> 所以，blocking IO的特点就是在IO执行的两个阶段都被block了。

## 非阻塞 I/O（nonblocking IO）

linux下，可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时，流程是这个样子：
![clipboard.png](pictures/Socket/bVm1c4.png)

当用户进程发出read操作时，如果kernel中的数据还没有准备好，那么它并不会block用户进程，而是立刻返回一个error。从用户进程角度讲 ，它发起一个read操作后，并不需要等待，而是马上就得到了一个结果。用户进程判断结果是一个error时，它就知道数据还没有准备好，于是它可以再次发送read操作。一旦kernel中的数据准备好了，并且又再次收到了用户进程的system call，那么它马上就将数据拷贝到了用户内存，然后返回。

> 所以，nonblocking IO的特点是用户进程需要**不断的主动询问**kernel数据好了没有。

## I/O 多路复用（ IO multiplexing）

IO multiplexing就是我们说的select，poll，epoll，有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select，poll，epoll这个function会不断的轮询所负责的所有socket，当某个socket有数据到达了，就通知用户进程。

![clipboard.png](pictures/Socket/bVm1c5.png)

`当用户进程调用了select，那么整个进程会被block`，而同时，kernel会“监视”所有select负责的socket，当任何一个socket中的数据准备好了，select就会返回。这个时候用户进程再调用read操作，将数据从kernel拷贝到用户进程。

> 所以，I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符，而这些文件描述符（套接字描述符）其中的任意一个进入读就绪状态，select()函数就可以返回。

这个图和blocking IO的图其实并没有太大的不同，事实上，还更差一些。因为这里需要使用两个system call (select 和 recvfrom)，而blocking IO只调用了一个system call (recvfrom)。但是，用select的优势在于它可以同时处理多个connection。

所以，如果处理的连接数不是很高的话，使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好，可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快，而是在于能处理更多的连接。）

在IO multiplexing Model中，实际中，对于每一个socket，一般都设置成为non-blocking，但是，如上图所示，整个用户的process其实是一直被block的。只不过process是被select这个函数block，而不是被socket IO给block。

## 异步 I/O（asynchronous IO）

linux下的asynchronous IO其实用得很少。先看一下它的流程：
![clipboard.png](pictures/Socket/bVm1c8.png)

用户进程发起read操作之后，立刻就可以开始去做其它的事。而另一方面，从kernel的角度，当它受到一个asynchronous read之后，首先它会立刻返回，所以不会对用户进程产生任何block。然后，kernel会等待数据准备完成，然后将数据拷贝到用户内存，当这一切都完成之后，kernel会给用户进程发送一个signal，告诉它read操作完成了。

## 总结

### blocking和non-blocking的区别

调用blocking IO会一直block住对应的进程直到操作完成，而non-blocking IO在kernel还准备数据的情况下会立刻返回。

### synchronous IO和asynchronous IO的区别

在说明synchronous IO和asynchronous IO的区别之前，需要先给出两者的定义。POSIX的定义是这样子的：
\- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
\- An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义，之前所述的blocking IO，non-blocking IO，IO multiplexing都属于synchronous IO。

有人会说，non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方，定义中所指的”IO operation”是指真实的IO操作，就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候，如果kernel的数据没有准备好，这时候不会block进程。但是，当kernel中数据准备好的时候，recvfrom会将数据从kernel拷贝到用户内存中，这个时候进程是被block了，在这段时间内，进程是被block的。

而asynchronous IO则不一样，当进程发起IO 操作之后，就直接返回再也不理睬了，直到kernel发送一个信号，告诉进程说IO完成。在这整个过程中，进程完全没有被block。

**各个IO Model的比较如图所示：**
![clipboard.png](pictures/Socket/bVm1c9.png)

通过上面的图片，可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中，虽然进程大部分时间都不会被block，但是它仍然要求进程去主动的check，并且当数据准备完成以后，也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人（kernel）完成，然后他人做完后发信号通知。在此期间，用户进程不需要去检查IO操作的状态，也不需要主动的去拷贝数据。

# 三 I/O 多路复用之select、poll、epoll详解

select，poll，epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制，一个进程可以监视多个描述符，一旦某个描述符就绪（一般是读就绪或者写就绪），能够通知程序进行相应的读写操作。但**select，poll，epoll本质上都是同步I/O**，因为他们都需要在读写事件就绪后自己负责进行读写，也就是说这个读写过程是阻塞的，而异步I/O则无需自己负责进行读写，异步I/O的实现会负责把数据从内核拷贝到用户空间。（这里啰嗦下）

## select(1024限制,返回后遍历read,write,except位图,复杂度n)

```
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
```

select 函数监视的文件描述符分3类，分别是writefds、readfds、和exceptfds。调用后select函数会阻塞，直到有描述副就绪（有数据 可读、可写、或者有except），或者超时（timeout指定等待时间，如果立即返回设为null即可），函数返回。当select函数返回后，可以 通过遍历fdset，来找到就绪的描述符。

select目前几乎在所有的平台上支持，其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制，在Linux上一般为1024，可以通过修改宏定义甚至重新编译内核的方式提升这一限制，但 是这样也会造成效率的降低。

## poll(无限制,不使用位图,使用链表描述符,复杂度n)

```
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
```

不同与select使用三个位图来表示三个fdset的方式，poll使用一个 pollfd的指针实现。

```
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};
```

pollfd结构包含了要监视的event和发生的event，不再使用select“参数-值”传递的方式。同时，pollfd并没有最大数量限制（但是数量过大后性能也是会下降）。 和select函数一样，poll返回后，需要轮询pollfd来获取就绪的描述符。

从上面看，select和poll都需要在返回后，**通过遍历文件描述符来获取已经就绪的socket**。事实上，同时连接的大量客户端在一时刻可能只有很少的处于就绪状态，因此随着监视的描述符数量的增长，其效率也会线性下降。

## epoll(零拷贝,无限制,O1)

epoll是在2.6内核中提出的，是之前的select和poll的增强版本。相对于select和poll来说，epoll更加灵活，

**1.没有描述符限制。**

**2.epoll使用一个文件描述符管理多个描述符，将用户关系的文件描述符的事件存放到内核的一个事件表中，这样在用户空间和内核空间的copy只需一次。**

3.复杂度为O1,**epoll可以理解为event poll**，不同于忙轮询和无差别轮询，epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是**事件驱动（每个事件关联上fd）**的，此时我们对这些流的操作都是有意义的。**（复杂度降低到了O(1)）**

### 零拷贝技术

类似于java的直接内存

参考文章:https://zhuanlan.zhihu.com/p/83398714

使用 mmap 的目的是将内核中读缓冲区（read buffer）的地址与用户空间的缓冲区（user buffer）进行映射，从而实现内核缓冲区与应用程序内存的共享，省去了将数据从内核读缓冲区（read buffer）拷贝到用户缓冲区（user buffer）的过程，然而内核读缓冲区（read buffer）仍需将数据到内核写缓冲区（socket buffer）.

### 一 epoll操作过程

epoll操作过程需要三个接口，分别如下：

```
int epoll_create(int size)；//创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)；
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
```

**1. int epoll_create(int size);**
**创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大**，这个参数不同于select()中的第一个参数，给出最大监听的fd+1的值，`参数size并不是限制了epoll所能监听的描述符最大个数，只是对内核初始分配内部数据结构的一个建议`。
当创建好epoll句柄后，它就会占用一个fd值，在linux下如果查看/proc/进程id/fd/，是能够看到这个fd的，所以在使用完epoll后，必须调用close()关闭，否则可能导致fd被耗尽。

**2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event \*event)；**
函数是对指定描述符fd执行op操作。
\- epfd：是epoll_create()的返回值。
\- op：表示op操作，用三个宏来表示：添加EPOLL_CTL_ADD，删除EPOLL_CTL_DEL，修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
\- fd：是需要监听的fd（文件描述符）
\- epoll_event：是告诉内核需要监听什么事，struct epoll_event结构如下：

```
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

//events可以是以下几个宏的集合：
EPOLLIN ：表示对应的文件描述符可以读（包括对端SOCKET正常关闭）；
EPOLLOUT：表示对应的文件描述符可以写；
EPOLLPRI：表示对应的文件描述符有紧急的数据可读（这里应该表示有带外数据到来）；
EPOLLERR：表示对应的文件描述符发生错误；
EPOLLHUP：表示对应的文件描述符被挂断；
EPOLLET： 将EPOLL设为边缘触发(Edge Triggered)模式，这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT：只监听一次事件，当监听完这次事件之后，如果还需要继续监听这个socket的话，需要再次把这个socket加入到EPOLL队列里
```

**3. int epoll_wait(int epfd, struct epoll_event \* events, int maxevents, int timeout);**
等待epfd上的io事件，最多返回maxevents个事件。
参数events用来从内核得到事件的集合，maxevents告之内核这个events有多大，这个maxevents的值不能大于创建epoll_create()时的size，参数timeout是超时时间（毫秒，0会立即返回，-1将不确定，也有说法说是永久阻塞）。该函数返回需要处理的事件数目，如返回0表示已超时。

### 二 工作模式

　epoll对文件描述符的操作有两种模式：**LT（level trigger）**和**ET（edge trigger）**。LT模式是默认模式，LT模式与ET模式的区别如下：
　　**LT模式**：当epoll_wait检测到描述符事件发生并将此事件通知应用程序，`应用程序可以不立即处理该事件`。下次调用epoll_wait时，会再次响应应用程序并通知此事件。
　　**ET模式**：当epoll_wait检测到描述符事件发生并将此事件通知应用程序，`应用程序必须立即处理该事件`。如果不处理，下次调用epoll_wait时，不会再次响应应用程序并通知此事件。

#### 1. LT模式

LT(level triggered)是缺省的工作方式，并且同时支持block和no-block socket.在这种做法中，内核告诉你一个文件描述符是否就绪了，然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作，内核还是会继续通知你的。

#### 2. ET模式

ET(edge-triggered)是高速工作方式，只支持no-block socket。在这种模式下，当描述符从未就绪变为就绪时，内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪，并且不会再为那个文件描述符发送更多的就绪通知，直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如，你在发送，接收或者接收请求，或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误）。但是请注意，如果一直不对这个fd作IO操作(从而导致它再次变成未就绪)，内核不会发送更多的通知(only once)

ET模式在很大程度上减少了epoll事件被重复触发的次数，因此效率要比LT模式高。epoll工作在ET模式的时候，**必须使用非阻塞套接口，以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。因为一旦阻塞了,就绪文件又不会重新发送触发,所以就会一直阻塞.**

#### 3. 总结

**假如有这样一个例子：**
\1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
\2. 这个时候从管道的另一端被写入了2KB的数据
\3. 调用epoll_wait(2)，并且它会返回RFD，说明它已经准备好读取操作
\4. 然后我们读取了1KB的数据
\5. 调用epoll_wait(2)......

**LT模式：**
如果是LT模式，那么在第5步调用epoll_wait(2)之后，仍然能受到通知。

**ET模式：**
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志，那么在第5步调用epoll_wait(2)之后将有可能会挂起，因为剩余的数据还存在于文件的输入缓冲区内，而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候，调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。

当使用epoll的ET模型来工作时，当产生了一个EPOLLIN事件后，
读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小，那么很有可能是缓冲区还有数据未读完，也意味着该次事件还没有处理完，所以还需要再次读取：

```
while(rs){
  buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
  if(buflen < 0){
    // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
    // 在这里就当作是该次事件已处理处.
    if(errno == EAGAIN){
        break;
    }
    else{
        return;
    }
  }
  else if(buflen == 0){
     // 这里表示对端的socket已正常关闭.
  }

 if(buflen == sizeof(buf){
      rs = 1;   // 需要再次读取
 }
 else{
      rs = 0;
 }
}
```

> **Linux中的EAGAIN含义**

Linux环境下开发经常会碰到很多错误(设置errno)，其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。
从字面上来看，是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。

例如，以 O_NONBLOCK的标志打开文件/socket/FIFO，如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回，read函数会返回一个错误EAGAIN，提示你的应用程序现在没有数据可读请稍后再试。
又例如，当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败，返回EAGAIN提示其再调用一次(也许下次就能成功)。

### 三 代码演示

下面是一段不完整的代码且格式不对，意在表述上面的过程，去掉了一些模板代码。

```
#define IPADDRESS   "127.0.0.1"
#define PORT        8787
#define MAXSIZE     1024
#define LISTENQ     5
#define FDSIZE      1000
#define EPOLLEVENTS 100

listenfd = socket_bind(IPADDRESS,PORT);

struct epoll_event events[EPOLLEVENTS];

//创建一个描述符
epollfd = epoll_create(FDSIZE);

//添加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);

//循环等待
for ( ; ; ){
    //该函数返回已经准备好的描述符事件数目
    ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
    //处理接收到的连接
    handle_events(epollfd,events,ret,listenfd,buf);
}

//事件处理函数
static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
     int i;
     int fd;
     //进行遍历;这里只要遍历已经准备好的io事件。num并不是当初epoll_create时的FDSIZE。
     for (i = 0;i < num;i++)
     {
         fd = events[i].data.fd;
        //根据描述符的类型和事件类型进行处理
         if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            handle_accpet(epollfd,listenfd);
         else if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,buf);
         else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,buf);
     }
}

//添加事件
static void add_event(int epollfd,int fd,int state){
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd){
     int clifd;
     struct sockaddr_in cliaddr;
     socklen_t  cliaddrlen;
     clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
     if (clifd == -1)
     perror("accpet error:");
     else {
         printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);                       //添加一个客户描述符和事件
         add_event(epollfd,clifd,EPOLLIN);
     }
}

//读处理
static void do_read(int epollfd,int fd,char *buf){
    int nread;
    nread = read(fd,buf,MAXSIZE);
    if (nread == -1)     {
        perror("read error:");
        close(fd); //记住close fd
        delete_event(epollfd,fd,EPOLLIN); //删除监听
    }
    else if (nread == 0)     {
        fprintf(stderr,"client close.\n");
        close(fd); //记住close fd
        delete_event(epollfd,fd,EPOLLIN); //删除监听
    }
    else {
        printf("read message is : %s",buf);
        //修改描述符对应的事件，由读改为写
        modify_event(epollfd,fd,EPOLLOUT);
    }
}

//写处理
static void do_write(int epollfd,int fd,char *buf) {
    int nwrite;
    nwrite = write(fd,buf,strlen(buf));
    if (nwrite == -1){
        perror("write error:");
        close(fd);   //记住close fd
        delete_event(epollfd,fd,EPOLLOUT);  //删除监听
    }else{
        modify_event(epollfd,fd,EPOLLIN);
    }
    memset(buf,0,MAXSIZE);
}

//删除事件
static void delete_event(int epollfd,int fd,int state) {
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

//修改事件
static void modify_event(int epollfd,int fd,int state){
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}

//注：另外一端我就省了
```

### 四 epoll总结

在 select/poll中，进程只有在调用一定的方法后，内核才对所有监视的文件描述符进行扫描，而**epoll事先通过epoll_ctl()来注册一 个文件描述符，一旦基于某个文件描述符就绪时，内核会采用类似callback的回调机制，迅速激活这个文件描述符，当进程调用epoll_wait() 时便得到通知**。(`此处去掉了遍历文件描述符，而是通过监听回调的的机制`。这正是epoll的魅力所在。)

**epoll的优点主要是一下几个方面：**
\1. 监视的描述符数量不受限制，它所支持的FD上限是最大可以打开文件的数目，这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右，具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对 于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的)，不过虽然linux上面创建进程的代价比较小，但仍旧是不可忽视的，加上进程间数据同步远比不上线程间同步的高效，所以也不是一种完美的方案。

1. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式，而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。

> 如果没有大量的idle -connection或者dead-connection，epoll的效率并不会比select/poll高很多，但是当遇到大量的idle- connection，就会发现epoll的效率大大高于select/poll。

<file_sep>/notes/LeetCode02.md
<!-- TOC -->

- [习惯问题:](#习惯问题)
- [测试用例](#测试用例)
- [交流](#交流)
- [为什么算法题取模要用10^9+7](#为什么算法题取模要用10^97)
- [[会议室2](https://leetcode-cn.com/problems/meeting-rooms-ii/)](#会议室2httpsleetcode-cncomproblemsmeeting-rooms-ii)
- [[@443. 压缩字符串](https://leetcode-cn.com/problems/string-compression/)](#443-压缩字符串httpsleetcode-cncomproblemsstring-compression)
- [[1239. 串联字符串的最大长度](https://leetcode-cn.com/problems/maximum-length-of-a-concatenated-string-with-unique-characters/)](#1239-串联字符串的最大长度httpsleetcode-cncomproblemsmaximum-length-of-a-concatenated-string-with-unique-characters)
- [[@@@@@297. 二叉树的序列化与反序列化](https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/)](#297-二叉树的序列化与反序列化httpsleetcode-cncomproblemsserialize-and-deserialize-binary-tree)
- [[103. 二叉树的锯齿形层次遍历](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/)](#103-二叉树的锯齿形层次遍历httpsleetcode-cncomproblemsbinary-tree-zigzag-level-order-traversal)
- [[@151. 翻转字符串里的单词](https://leetcode-cn.com/problems/reverse-words-in-a-string/)](#151-翻转字符串里的单词httpsleetcode-cncomproblemsreverse-words-in-a-string)
- [[24. 两两交换链表中的节点](https://leetcode-cn.com/problems/swap-nodes-in-pairs/)](#24-两两交换链表中的节点httpsleetcode-cncomproblemsswap-nodes-in-pairs)
- [[25. K 个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/)](#25-k-个一组翻转链表httpsleetcode-cncomproblemsreverse-nodes-in-k-group)
- [[@@@@@10. 正则表达式匹配](https://leetcode-cn.com/problems/regular-expression-matching/)](#10-正则表达式匹配httpsleetcode-cncomproblemsregular-expression-matching)
- [[@8. 字符串转换整数 (atoi)](https://leetcode-cn.com/problems/string-to-integer-atoi/)](#8-字符串转换整数-atoihttpsleetcode-cncomproblemsstring-to-integer-atoi)
- [[@235. 二叉搜索树的最近公共祖先](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/)](#235-二叉搜索树的最近公共祖先httpsleetcode-cncomproblemslowest-common-ancestor-of-a-binary-search-tree)
- [[@236. 二叉树的最近公共祖先](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/)](#236-二叉树的最近公共祖先httpsleetcode-cncomproblemslowest-common-ancestor-of-a-binary-tree)
- [@循环节长度](#循环节长度)
- [两个数字字符串相加，区分正负](#两个数字字符串相加区分正负)
- [[@419. 甲板上的战舰](https://leetcode-cn.com/problems/battleships-in-a-board/)](#419-甲板上的战舰httpsleetcode-cncomproblemsbattleships-in-a-board)
- [[@@398. 随机数索引](https://leetcode-cn.com/problems/random-pick-index/)](#398-随机数索引httpsleetcode-cncomproblemsrandom-pick-index)
- [@@蓄水池抽样问题](#蓄水池抽样问题)
- [@@随机数的生成](#随机数的生成)
- [[面试题 16.11. 跳水板](https://leetcode-cn.com/problems/diving-board-lcci/)](#面试题-1611-跳水板httpsleetcode-cncomproblemsdiving-board-lcci)
- [[@@@@581. 最短无序连续子数组](https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/)](#581-最短无序连续子数组httpsleetcode-cncomproblemsshortest-unsorted-continuous-subarray)
- [[@@93. 复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/)](#93-复原ip地址httpsleetcode-cncomproblemsrestore-ip-addresses)
- [[134. 加油站](https://leetcode-cn.com/problems/gas-station/)](#134-加油站httpsleetcode-cncomproblemsgas-station)
- [[@@@116. 填充每个节点的下一个右侧节点指针](https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/)](#116-填充每个节点的下一个右侧节点指针httpsleetcode-cncomproblemspopulating-next-right-pointers-in-each-node)
- [[@@@90. 子集 II](https://leetcode-cn.com/problems/subsets-ii/)](#90-子集-iihttpsleetcode-cncomproblemssubsets-ii)
- [[13. 罗马数字转整数](https://leetcode-cn.com/problems/roman-to-integer/)](#13-罗马数字转整数httpsleetcode-cncomproblemsroman-to-integer)
- [[@348. 判定井字棋胜负](https://leetcode-cn.com/problems/design-tic-tac-toe/)](#348-判定井字棋胜负httpsleetcode-cncomproblemsdesign-tic-tac-toe)
- [[114. 二叉树展开为链表](https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/)](#114-二叉树展开为链表httpsleetcode-cncomproblemsflatten-binary-tree-to-linked-list)
- [[48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/)](#48-旋转图像httpsleetcode-cncomproblemsrotate-image)
- [@连续子串=连续子序列!=子序列 ;](#连续子串连续子序列子序列-)
- [@@和0为的连续最长子序列(子串,和为k)](#和0为的连续最长子序列子串和为k)
- [@@最大长度的01连续子序列](#最大长度的01连续子序列)
- [[全排列](https://leetcode-cn.com/problems/permutations/)](#全排列httpsleetcode-cncomproblemspermutations)
- [012组合数](#012组合数)
- [求1到1000000的质数](#求1到1000000的质数)
- [三角形](#三角形)
- [链表相交的节点](#链表相交的节点)
- [[@@@142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/)](#142-环形链表-iihttpsleetcode-cncomproblemslinked-list-cycle-ii)
- [@@相亲数](#相亲数)
- [@@@@@@@@链表快排.](#链表快排)
- [[@@@@@148. 排序链表](https://leetcode-cn.com/problems/sort-list/)](#148-排序链表httpsleetcode-cncomproblemssort-list)
- [[@@@表达式求值](https://www.nowcoder.com/questionTerminal/3e483fe3c0bb447bb17ffb3eeeca78ba?f=discussion)](#表达式求值httpswwwnowcodercomquestionterminal3e483fe3c0bb447bb17ffb3eeeca78bafdiscussion)

<!-- /TOC -->

#### 习惯问题:

列出具体的数组,来演示思考过程

先跟面试官沟通题目的意思

表达思路

把可能出现的问题写下来,注意边界情况  判空, 单数双数,正负数,有相同重复数据(map,index),

写完后把代码规整起来

主动分析时间复杂度和空间复杂度,寻找优化的点.

#### 测试用例

检测正确功能的测试用例

while(left<right)的循环内部,记得left++,right--

double类型

边界情况

非法输入

判空,

单数双数

正负数

0的情况

定位角色混淆不明确.

有相同重复数据(map,index,非重复组合)

值溢出

输入输出不符合规范

矩阵是不是方阵

如果是直角三角形的时候，也有可能是等腰三角形；

计算直角三角形的时候，要考虑溢出。

#### 交流

**交流很重要，要互动(有没有负数,允不允许结果重复)**

注重**代码质量（编程风格、命名规范、异常值处理）**

各种**follow up**以及要你设计尽可能多的**测试用例**。

#### 为什么算法题取模要用10^9+7

<img src="pictures/LeetCode02/image-20200319100206442.png" alt="image-20200319100206442" style="zoom:50%;" />

#### [会议室2](https://leetcode-cn.com/problems/meeting-rooms-ii/)

```java

思路:startTime排序,每到一个startTime,usedRoom就+1.如果在该时间之前有结束的会议室(endTime[endPointer]<=startTime[startPointer]).usedRoom就-1;
public int minMeetingRooms(int[][] intervals) {
        int len = intervals.length;

        int max = 0;
        int[] startTime = new int[len];
        int[] endTime = new int[len];
        for (int i = 0; i < len; i++) {
            startTime[i] = intervals[i][0];
            endTime[i] = intervals[i][1];
        }
        Arrays.sort(startTime);
        Arrays.sort(endTime);
        int usedRooms = 0;
        int endPointer = 0;
        for (int startPointer = 0; startPointer < len; ) {
            if (endTime[endPointer] <= startTime[startPointer]) {
                usedRooms--;
                endPointer++;
            }
            usedRooms++;
            startPointer++;
        }
        return usedRooms;
    }
```

#### [@443. 压缩字符串](https://leetcode-cn.com/problems/string-compression/)

write read,chBegin的妙用

if (read == chars.length - 1 || chars[read] != chars[read + 1])  这两个条件很重要

```java
 public int compress(char[] chars) {
        int read = 0;
        int write = 0;
        int chBegin = 0;
        for (read = 0; read < chars.length; read++) {
            if (read == chars.length - 1 || chars[read] != chars[read + 1]) {
                int count = read - chBegin + 1;
                chars[write++] = chars[read];
                if (count >= 2) {
                    String temp = count + "";
                    for (int i = 0; i < temp.length(); i++) {
                        chars[write++] = temp.charAt(i);
                    }
                }
                chBegin = read + 1;
            }
        }
        return write;
    }
```

#### [1239. 串联字符串的最大长度](https://leetcode-cn.com/problems/maximum-length-of-a-concatenated-string-with-unique-characters/)

回溯的题目,没啥好说的,熟练了

```java
int maxLen = 0;
    HashSet<Character> set = new HashSet<>();

    public int maxLength(List<String> arr) {
        bracktracing(arr, 0, 0);
        return maxLen;
    }

    private void bracktracing(List<String> arr, int index, int len) {
        if (index >= arr.size()) {
            return;
        }
        for (int i = index; i < arr.size(); i++) {
            String temp = arr.get(i);
            if (isUnique(temp)) {
                maxLen = Math.max(maxLen, len + temp.length());
                for (int j = 0; j < temp.length(); j++) {
                    set.add(temp.charAt(j));
                }

                bracktracing(arr, i + 1, len + temp.length());

                for (int j = 0; j < temp.length(); j++) {
                    set.remove(temp.charAt(j));
                }
            }
        }
    }

    private boolean isUnique(String temp) {
        for (int i = 0; i < temp.length(); i++) {
            if (set.contains(temp.charAt(i))) {
                int cutIndex = i;
                for (int j = 0; j < cutIndex; j++) {
                    set.remove(temp.charAt(j));
                }
                return false;
            } else {
                set.add(temp.charAt(i));
            }
        }
        return true;
    }
```

#### [@@@@@297. 二叉树的序列化与反序列化](https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/)

@为空返回#,左右子树用" "分隔开

@思路2 是错误的,  原因是 data是局部变量,而非全局变量,因为data是string类型,data的改变会被传入新的函数root.left = deserialize(data);但是当执行完root.left之后.回到root.right = deserialize(data); data的值不会受root.left的影响.

@序列出来的效果像这样:"1 2 # # 3 4 # # 5 # #"

```java
 //return "#"很关键
   String data = null;

    public String serialize(TreeNode root) {
        if (root == null) {
            return "#";
        }
        return root.val + " " + serialize(root.left) + " " + serialize(root.right);
    }

    public TreeNode deserialize(String str) {
        this.data = str;
        return deserialize();
    }

    private TreeNode deserialize() {
        if (data == null || data.length() == 0) {
            return null;
        }
        String rootValStr = null;
        int rootCutIndex = data.indexOf(" ");
		//切割rootValStr
        if (rootCutIndex == -1) {
            rootValStr = data;
        } else {
            rootValStr = data.substring(0, rootCutIndex);
        }
		//切割剩余的Str
        if (rootCutIndex == -1) {
            data = "";
        } else {
            data = data.substring(rootCutIndex + 1);
        }
		//rootValStr作解析
        if (rootValStr.equals("#")) {
            return null;
        } else {
            TreeNode root = new TreeNode(Integer.parseInt(rootValStr));
            root.left = deserialize();
            root.right = deserialize();
            return root;
        }
    }

错误的反序列化方式:
错在data不是全局变量.
 public TreeNode deserialize2(String data) {
        if (data == null || data.length() == 0) {
            return null;
        }
        int cutIndex = 0;
        for (cutIndex = 0; cutIndex < data.length(); cutIndex++) {
            if (data.charAt(cutIndex) == ' ') {
                break;
            }
        }
        String rootVal = data.substring(0, cutIndex);
        if (cutIndex < data.length()) {
            data = data.substring(cutIndex + 1);
        } else {
            data = "";
        }
        if (rootVal.equals("#")) {
            return null;
        }
        TreeNode root = new TreeNode(Integer.parseInt(rootVal));
        root.left = deserialize(data);
        root.right = deserialize(data);
        return root;
    }

```

#### [103. 二叉树的锯齿形层次遍历](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/)

```java
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
    Queue<TreeNode> queue = new LinkedList<>();
    int level = 1;
    int count = 0;
    List<List<Integer>> ret = new ArrayList<>();
    if (root == null) {
        return ret;
    }
    count = 1;
    queue.add(root);
    while (!queue.isEmpty()) {
        List<Integer> list = new ArrayList<>();
        int tempCount = 0;
        for (int i = 0; i < count; i++) {
            TreeNode node = queue.poll();
            if (level % 2 == 0) {
                list.add(0, node.val);
            } else {
                list.add(node.val);
            }
            if (node.left != null) {
                queue.add(node.left);
                tempCount++;
            }
            if (node.right != null) {
                queue.add(node.right);
                tempCount++;
            }
        }
        level++;
        count = tempCount;
        ret.add(list);
    }
    return ret;
}
```

#### [@151. 翻转字符串里的单词](https://leetcode-cn.com/problems/reverse-words-in-a-string/)

@trim

@从后往前

@StringBuilder.append()  .deleter()  .length()

```java
public String reverseWords(String s) {
        s = s.trim();
        int len = s.length();
        StringBuilder ret = new StringBuilder();
        int wordEnd = len - 1;
        int read = len - 1;
        while (read >= 0) {
            while (read >= 0 && s.charAt(read) != ' ') {
                read--;
            }
            String temp = s.substring(read + 1, wordEnd + 1);
            //最后一个单词末尾不append " "
            if (read == -1) {
                ret.append(temp);
                return ret.toString();
            } else {
                ret.append(temp + " ");
            }
            while (read >= 0 && s.charAt(read) == ' ') {
                read--;
            }
            wordEnd = read;
        }
        return ret.toString();
    }
```

#### [24. 两两交换链表中的节点](https://leetcode-cn.com/problems/swap-nodes-in-pairs/)

```java
 public ListNode swapPairs(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode pHead = new ListNode(-1);
        ListNode pNode = pHead;

        ListNode preNode = head;
        ListNode laterNode = head.next;

        while (preNode != null && laterNode != null) {
            ListNode temp = laterNode.next;
            //把laterNode插入
            laterNode.next = null;
            pNode.next = laterNode;
            pNode = pNode.next;
            //把preNode插入
            preNode.next = null;
            pNode.next = preNode;
            pNode = pNode.next;

            //pre 和 later都后移
            preNode = temp;
            if (temp != null) {
                laterNode = temp.next;
            }
        }
        //如果末尾还剩余一个节点,单独插入
        if (preNode != null) {
            preNode.next = null;
            pNode.next = preNode;
        }
        return pHead.next;
    }
```

#### [25. K 个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/)

@ k个一组,头插法翻转

@不需要先遍历k个一组是否满足k个,可以先翻转着,如果不满足k个,再翻转回来.

```java
public ListNode reverseKGroup(ListNode head, int k) {
        ListNode node = head;
        ListNode retHead = new ListNode(-1);
        ListNode retEnd = retHead;
        while (true) {
            //翻转
            int count = 0;
            ListNode reverseHead = new ListNode(-1);
            ListNode lastNode = null;
            //注意记录lastNode,是count为0的时候
            while (node != null && count < k) {
                ListNode temp = node.next;
                node.next = reverseHead.next;
                reverseHead.next = node;
                if (count == 0) {
                    lastNode = node;
                }
                node = temp;
                count++;
            }
            if (count == k) {
                retEnd.next = reverseHead.next;
                retEnd = lastNode;
            } else {
                ListNode remainList = reverse(reverseHead);
                retEnd.next = remainList;
                break;
            }
        }
        return retHead.next;

    }

    private ListNode reverse(ListNode head) {
        ListNode pHead = new ListNode(-1);
        ListNode node = head.next;
        while (node != null) {
            ListNode temp = node.next;
            node.next = pHead.next;
            pHead.next = node;
            node = temp;
        }
        return pHead.next;
    }
```

#### [@@@@@10. 正则表达式匹配](https://leetcode-cn.com/problems/regular-expression-matching/)

```
@为什么是 for (int i = 0; i < len1; i++) {
            for (int j = 0; j < len2; j++) {

 因为我dp[i][j]的含义是p的前j个字符串匹配  s的前i个.
 dp[i][j]需要用到 i-1,j-1  所以 for i  for j;

 而且用j在前,i在后也可以
 for (int j = 0; j < len2; j++) {
 			for (int i = 0; i < len1; i++) {

```

```java

   public boolean isMatch(String s, String p) {
        if (s == null || p == null) {
            return false;
        }
        int len1 = s.length();
        int len2 = p.length();
        boolean[][] dp = new boolean[len1 + 1][len2 + 1];
        //边界的初始化.
        dp[0][0] = true;
        //初始化"" 与p的匹配关系      "" 和 a*a*a*是可以匹配的
        for (int j = 1; j <= len2; j++) {
            if (p.charAt(j - 1) == '*' && dp[0][j - 2]) {
                dp[0][j] = true;
            }
        }
        //s不为空   p 为""  对应 dp[i][0] 全应为false

        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                // xxp   xxp     或     xxs    xx.
                if (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.') {
                    dp[i][j] = dp[i - 1][j - 1];
                } else if (p.charAt(j - 1) == '*') {
                    //两个i-1  两个 j-2
                    //xxp     xxp*    i-1  和  j-2   p*  匹配1个
                    //xxppp   xxp*    i-1 和  j   p* 匹配多个
                    // xxp    xxpp*  i  和 j-2   p*匹配0个
                    if (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.') {
                        dp[i][j] = dp[i - 1][j - 2] || dp[i - 1][j] || dp[i][j - 2];
                    } else {
                        // xx  要和  xxp*匹配   i  和  j-2   p*匹配0个
                        dp[i][j] = dp[i][j - 2];
                    }
                }
            }
        }
        return dp[len1][len2];
    }

```

#### [@8. 字符串转换整数 (atoi)](https://leetcode-cn.com/problems/string-to-integer-atoi/)

@trim前判空,trim完了要判断长度,防止越界

@

```
if(ret<littleLimit){
	return ....
}
if(ret==littleLimit){
	if(ret*10<limit+digit){
		return ...
	}
}
ret = ret*10-digit;
```

```java

public int myAtoi(String str) {
    	if(str==null){
        	return 0;
        }
        str = str.trim();
        if (str == null || str.length() == 0)
            return 0;
        int len = str.length();
        boolean isNegative = false;
        boolean firstSign = false;
        char[] chars = str.toCharArray();

        int limit = 0;//用正数最大值的相反数作限制
        if (chars[0] == '-') {
            isNegative = true;
            firstSign = true;
            limit = Integer.MIN_VALUE;
        } else if (chars[0] == '+') {
            isNegative = false;
            firstSign = true;
            limit = -Integer.MAX_VALUE;
        } else {
            isNegative = false;
            firstSign = false;
            limit = -Integer.MAX_VALUE;
        }

        int littleLimit = limit / 10;
        int digit = 0;
        int result = 0;
        for (int i = firstSign ? 1 : 0; i < len; i++) {
            if (!(chars[i] >= '0' && chars[i] <= '9'))
                break;
            digit = chars[i] - '0';
            if (result < littleLimit)
                return isNegative?Integer.MIN_VALUE:Integer.MAX_VALUE;
            if (result * 10 < limit + digit)
                //注意!!!!!!!改进!!!!!!
                //这里不可以写result*10-digit < limit  左边会越界,影响比较大小的真实逻辑
                return isNegative?Integer.MIN_VALUE:Integer.MAX_VALUE;
            result = result * 10 - digit;
        }
        return isNegative ? result : -result;
    }

```

#### [@235. 二叉搜索树的最近公共祖先](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/)

@二叉搜索树的特点是,找到第一个可以把p和q区分开的root.val,就是最近的root.val.

所以本题也不需要后序遍历.

```java
递归:起名有问题,其实不是后序遍历,从根往下走即可

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) {
            return null;
        }
        return lastOrder(root, p, q);
    }

    private TreeNode lastOrder(TreeNode root, TreeNode p, TreeNode q) {

        if (root.val > p.val && root.val > q.val) {
            return lastOrder(root.left, p, q);
        } else if (root.val < p.val && root.val < q.val) {
            return lastOrder(root.right, p, q);
        } else {
            return root;
        }
    }

非递归:也不是后序遍历,直接从根往下走即可

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null)
            return null;
        int pVal = p.val;
        int qVal = q.val;
        TreeNode node = root;
        while(node!=null){
            if(node.val>pVal&&node.val>qVal)
                node = node.left;
            else if(node.val<pVal&&node.val<qVal)
                node = node.right;
            else
                return node;
        }
        return node;
    }

```

#### [@236. 二叉树的最近公共祖先](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/)

```java
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        return preOrder(root, p, q);
    }

    private TreeNode preOrder(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) {
            return root;
        }
        if (root == p || root == q) {
            return root;
        }
        TreeNode left = preOrder(root.left, p, q);
        TreeNode right = preOrder(root.right, p, q);

        if (left != null && right != null) {
            return root;
        } else if (left == null && right == null) {
            return null;
        } else {
            return left == null ? right : left;
        }
    }
```

#### @循环节长度

两个整数做除法，有时会产生循环小数，其循环部分称为：循环节。
比如，11/13=6=>0.846153846153….. 其循环节为[846153] 共有6位。

https://blog.csdn.net/weixin_41514525/article/details/97121292

```java
思路:如果11/13 是循环的.那么将来的被除数中肯定还会出现11
private int getCyc(int n, int m) {
        if (m == 0) {
            return -1;
        }
        int remain = n % m;
        int remainFlag = remain;
        int count = 0;
        do {
            count++;
            remain = remain * 10 % m;
            if (remain == 0) {
                return -1;
            }
        } while (remain != remainFlag);
        return count;
    }

```

#### 两个数字字符串相加，区分正负

```java

1.正负号另算,用大正数减去小正数
2. 比如9887 - 92
3.  首位-1,   末位借10 ,  其他位都借9.
也就是8887 加上   99(10) -92
4.最后8887+ 908

public String addStrings(String num1, String num2) {
        int i = num1.length() - 1;
        int j = num2.length() - 1;
        int carry = 0;
        StringBuilder ret = new StringBuilder();
        while (i >= 0 || j >= 0) {
            int a = i >= 0 ? num1.charAt(i) - '0' : 0;
            int b = j >= 0 ? num2.charAt(j) - '0' : 0;
            int sum = a + b + carry;
            int add = sum % 10;
            carry = sum / 10;
            ret.append(add);
            i--;
            j--;
        }
        if (carry > 0) {
            ret.append(carry);
        }
        return ret.reverse().toString();
    }

```

#### [@419. 甲板上的战舰](https://leetcode-cn.com/problems/battleships-in-a-board/)

```java
如果题设没有特殊规则和要求的话，常规做法就是使用BFS或者DFS扫描，计算连通图的个数。
但是题目要求只扫描一次，且O(1)的额外空间，那么就只能从题目给出的特殊规则出发了。
特殊规则：连通图是1xN或者Nx1的长条形的，且不会相交（之间有.分隔）。
解法：扫描到X时，如果上方或者左方也是X，则不计数，否则计数加1。

class Solution {
public:
    int countBattleships(vector<vector<char>>& board) {
        int nCount = 0;
        for (int i = 0; i < board.size(); ++i) {
            for (int j = 0; j < board[i].size(); ++j) {
                if (board[i][j] == 'X') {
                    if (i > 0 && board[i - 1][j] == 'X' ||
                        j > 0 && board[i][j - 1] == 'X')
                        continue;
                    ++nCount;
                }
            }
        }
        return nCount;
    }
};

```

#### [@@398. 随机数索引](https://leetcode-cn.com/problems/random-pick-index/)

**第k个数被选中的概率是  1/k.  前1~k-1个数平分 (k-1)/k的可能性(为什么? 用归纳推理可知, k=1时,概率为1,k=2时,两个元素概率为 1/2,1/2. k=3时,即为k的普遍情况.),所以最终每个元素都获得 1/k被选中的可能性.**

#### @@蓄水池抽样问题

首先从最简单的例子出发：数据流只有一个数据。我们接收数据，发现数据流结束了，直接返回该数据，该数据返回的概率为1。

看来很简单，那么我们试试难一点的情况：假设数据流里有两个数据。我们读到了第一个数据，这次我们不能直接返回该数据，因为数据流没有结束。我们继续读取第二个数据，发现数据流结束了。因此我们只要保证以相同的概率返回第一个或者第二个数据就可以满足题目要求。因此我们生成一个0到1的随机数R,如果R小于0.5我们就返回第一个数据，如果R大于0.5，返回第二个数据。

接着我们继续分析有三个数据的数据流的情况。为了方便，我们按顺序给流中的数据命名为1、2、3。我们陆续收到了数据1、2和前面的例子一样，我们只能保存一个数据，所以必须淘汰1和2中的一个。应该如何淘汰呢？不妨和上面例子一样，我们按照二分之一的概率淘汰一个，例如我们淘汰了2。继续读取流中的数据3，发现数据流结束了，我们知道在长度为3的数据流中，如果返回数据3的概率为1/3,那么才有可能保证选择的正确性。也就是说，目前我们手里有1,3两个数据，我们通过一次随机选择，以1/3的概率留下数据3，以2/3的概率留下数据1。那么数据1被最终留下的概率是多少呢？

数据1被留下：（1/2）(2/3) = 1/3
数据2被留下概率：（1/2）(2/3) = 1/3
数据3被留下概率：1/3

```java
 Random random = new Random();
int i = random.nextInt(100);

蓄水池抽样问题.

 private int[] nums = null;

    public Solution(int[] nums) {
        this.nums = nums;
    }

    public int pick(int target) {
        int ret = 0;
        int cnt = 0;
        Random r = new Random();
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == target) {
                if (cnt == 0) {
                    cnt++;
                    ret = i;
                } else {
                    cnt++;
                    if (r.nextInt() % cnt == 0) {
                        ret = i;
                    }
                }
            }
        }
        return ret;
    }
```

#### @@随机数的生成

```java
Random函数的官方注释
random.nextInt(5);  0~4
//Returns a pseudorandom, uniformly distributed {@code int} value
//between 0 (inclusive) and the specified value (exclusive),
```

https://blog.csdn.net/wangruitao1991/article/details/51678815

已知rand5生成rand7.

1. 首先用  5*(rand5-1)生成  0  5 10 15...20

2. 再用  5*(rand5-1)+rand5  生成  1~25均匀整数

3. 因为7在25下的最大倍数是21,所以 如果步骤2里面生成的数字大于21,就重新生成,小于21就进行第4步

4. 1~21之间的数,用来%7, 求出0~6.  再+1 即可 1~7的随机分布.

```
int Rand7(){
    int x = ~(1<<31); // max int
    while(x > 21)
        x = 5 * (Rand5() - 1) + Rand5() // Rand25
    return x%7 + 1;
}
```

再比如用rand100生成rand150

```
int rand150(){
	int x = 0;
	x = 100* ( rand100() - 1 ) +rand100();
	//10000> 150*60=9900
	while(x>9900){
		x = 100* ( rand100() - 1 ) +rand100();
	}
	return x%150+1;
}
```

#### [面试题 16.11. 跳水板](https://leetcode-cn.com/problems/diving-board-lcci/)

@@@@注意 数组中的diff的次数是从0 到k

```java
public int[] divingBoard(int shorter, int longer, int k) {
    int diff = longer - shorter;

    if (k == 0) {
        return new int[0];
    }
    if (shorter == longer) {
        return new int[]{k * shorter};
    }
    //diff的个数是从0到k  所以是k+1
    int[] ret = new int[k + 1];
    for (int i = 0; i <= k; i++) {
        ret[i] = shorter * k + diff * i;
    }
    return ret;
}
```

#### [@@@@581. 最短无序连续子数组](https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/)

@@@@注意 求得是无序子数组中的**最小值 和最大值**

@思想是无序子数组中最小元素的正确位置可以决定左边界，最大元素的正确位置可以决定右边界。

```java
public int findUnsortedSubarray(int[] nums) {
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        boolean flag = false;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] < nums[i - 1]) {
                flag = true;
            }
            if (flag) {
                min = Math.min(min, nums[i]);
            }
        }
        flag = false;
        for (int i = nums.length - 2; i >= 0; i--) {
            if (nums[i] > nums[i + 1]) {
                flag = true;
            }
            if (flag) {
                max = Math.max(max, nums[i]);
            }
        }
        int start = 0;
        int end = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] > min) {
                start = i;
                break;
            }
        }
        for (int i = nums.length - 1; i >= 0; i--) {
            if (nums[i] < max) {
                end = i;
                break;
            }
        }
        return start == end ? 0 : end - start + 1;
    }
```

```java
方法二:如果左边的最大值大于右边的最小值,则最小值应该放在该位置上
    	同理,如果右边的最小值小于左边的最大值,则最大值应该放在该位置上
    	最后两个位置相减
 public int findUnsortedSubarray(int[] nums) {
        int len = nums.length;
        int[] leftMax = new int[len];
        int[] rightMin = new int[len];
        int max = nums[0];
        int min = nums[len - 1];
        leftMax[0] = max;
        rightMin[len - 1] = min;
        for (int i = 0; i < len; i++) {
            if (nums[i] > max) {
                max = nums[i];

            }
            leftMax[i] = max;
        }
        for (int i = len - 1; i >= 0; i--) {
            if (nums[i] < min) {
                min = nums[i];
            }
            rightMin[i] = min;
        }
        int leftCut = 0;
        int rightCut = len - 1;
        for (int i = 0; i < len; i++) {
            if (leftMax[i] > rightMin[i]) {
                leftCut = i;
                break;
            }
        }
        for (int i = len - 1; i >= 0; i--) {
            if (rightMin[i] < leftMax[i]) {
                rightCut = i;
                //应对 3  2  1
                return rightCut - leftCut + 1;
//                break;
            }
        }
        //应对  1 2  3
        return 0;
    }
```

#### [@@93. 复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/)

@StringBuilder用于回溯

@ip的条件

1. cutCount = 4,

2. index==len,

3. Integer.parseInt(temp)<=255

4. 若temp.charAt(0)=='0'只能为一位0不能是001,012,08这种类型.

```java
 List<String> ret = null;
    int len = 0;

    public List<String> restoreIpAddresses(String s) {
        ret = new ArrayList<String>();
        if (s == null || s.length() == 0) {
            return ret;
        }
        len = s.length();
        backtracing(new StringBuilder(), s, 0, 1);
        return ret;
    }

    private void backtracing(StringBuilder path, String s, int index, int cutCount) {
        if (index > len || cutCount > 5) {
            return;
        }
        if (index == len && cutCount == 5) {
            path.delete(path.length() - 1, path.length());
            ret.add(path.toString());
            path.append(".");
            return;
        }
        //不要忘记j<len
        for (int j = index; j < len && j < index + 3; j++) {
            String temp = s.substring(index, j + 1);
            //index为0,同时j为初始的index
            if (s.charAt(index) == '0' && j == index) {
                path.append(temp);
                path.append(".");
                int addedLen = temp.length() + 1;
                backtracing(path, s, j + 1, cutCount + 1);
                int pathLen = path.length();
                path.delete(pathLen - addedLen, pathLen);
                //首位不为0,并且求值小于255
            } else if (s.charAt(index) != '0' && Integer.parseInt(temp) <= 255) {
                path.append(temp);
                path.append(".");
                int tempLen = temp.length() + 1;
                backtracing(path, s, j + 1, cutCount + 1);
                int pathLen = path.length();
                path.delete(pathLen - tempLen, pathLen);
            }
        }
    }
```

#### [134. 加油站](https://leetcode-cn.com/problems/gas-station/)

```java
 boolean finded = false;

    public int canCompleteCircuit(int[] gas, int[] cost) {
        for (int i = 0; i < gas.length; i++) {
            carRun(gas, cost, i);
            if (finded) {
                return i;
            }
        }
        return -1;
    }

    private void carRun(int[] gas, int[] cost, int begin) {
        int tank = 0;
        for (int i = begin; i < gas.length; i++) {
            tank += gas[i];
            tank -= cost[i];
            if (tank < 0) {
                return;
            }
        }
        for (int i = 0; i <= begin; i++) {
            tank += gas[i];
            tank -= cost[i];
            if (tank < 0) {
                return;
            }
        }
        finded = true;
    }
```

#### [@@@116. 填充每个节点的下一个右侧节点指针](https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/)

@每个递归,负责连两条线.

@@@

  	Node next = root.next;
        if (right != null && next != null) {
            right.next = next.left;
        }

```java
public Node connect(Node root) {
        if (root == null) {
            return null;
        }
        Node left = root.left;
        Node right = root.right;
        if (left != null) {
            left.next = right;
        }

        Node next = root.next;
        if (right != null && next != null) {
            right.next = next.left;
        }

        connect(root.left);
        connect(root.right);
        return root;
    }
```

#### [@@@90. 子集 II](https://leetcode-cn.com/problems/subsets-ii/)

@排序+第二次碰到相同的数字就跳过(注意是第二次碰到再跳过)比如

1  2   2  3

```java
 public List<List<Integer>> ret = null;
    int[] nums = null;

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        ret = new ArrayList<>();
        if (nums == null) {
            return ret;
        }
        this.nums = nums;
        Arrays.sort(nums);
        backtacing(0, new ArrayList<Integer>());
        return ret;
    }

    public void backtacing(int index, ArrayList<Integer> path) {
        if (index > nums.length) {
            return;
        }
        ret.add(new ArrayList<Integer>(path));
        for (int i = index; i < nums.length; i++) {
            while (i > index && i < nums.length && nums[i - 1] == nums[i]) {
                i++;
            }
            if (i < nums.length) {
                path.add(nums[i]);
                backtacing(i + 1, path);
                path.remove(path.size() - 1);
            }
        }

    }
```

#### [13. 罗马数字转整数](https://leetcode-cn.com/problems/roman-to-integer/)

@因为是3999以内,最大位就是M(千).另外需要注意的就是如果preNum<nowNum,则pre代表的是要减去的值.

通常情况下，罗马数字中小的数字在大的数字的右边。但也存在特例，例如 4 不写做 IIII，而是 IV。数字 1 在数字 5 的左边，所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地，数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况：

I 可以放在 V (5) 和 X (10) 的左边，来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边，来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边，来表示 400 和 900。
给定一个罗马数字，将其转换成整数。输入确保在 1 到 3999 的范围内。

来源：力扣（LeetCode）
链接：https://leetcode-cn.com/problems/roman-to-integer
著作权归领扣网络所有。商业转载请联系官方授权，非商业转载请注明出处。

```java
 public int romanToInt(String s) {
        int preNum = getValue(s.charAt(0));
        int num = 0;
        int sum = 0;
        for (int i = 1; i < s.length(); i++) {
            num = getValue(s.charAt(i));
            if (preNum < num) {
                sum -= preNum;
            } else {
                sum += preNum;
            }
            preNum = num;
        }
        sum += preNum;
        return sum;

    }

    private int getValue(char ch) {
        switch (ch) {
            case 'I':
                return 1;
            case 'V':
                return 5;
            case 'X':
                return 10;
            case 'L':
                return 50;
            case 'C':
                return 100;
            case 'D':
                return 500;
            case 'M':
                return 1000;
            default:
                return 0;
        }
    }
```

#### [@348. 判定井字棋胜负](https://leetcode-cn.com/problems/design-tic-tac-toe/)

@但是本题没有判断 同一个玩家在某个点处重复下棋的情况,当然题目中也没要求有这种判断.

@制造一个记录每行,每列的数组,和两个记录对角线棋子数的数字.

一旦某个计数满足len,就获胜了

```java
    int[] rowRem = null;
    int[] colRem = null;
    int diagnol = 0;
    int antiDiagnol = 0;
    int len = 0;

    public TicTacToe(int n) {
        len = n;
        rowRem = new int[n];
        colRem = new int[n];
    }

    public int move(int row, int col, int player) {
        int toAdd = player == 1 ? 1 : -1;
        rowRem[row] += toAdd;
        colRem[col] += toAdd;
        if (row == col) {
            diagnol += toAdd;
        }
        if (row + col == len - 1) {
            antiDiagnol += toAdd;
        }
        if (Math.abs(rowRem[row]) == len ||
                Math.abs(colRem[col]) == len ||
                Math.abs(diagnol) == len ||
                Math.abs(antiDiagnol) == len) {
            return player;
        }
        return 0;
    }
```

#### [114. 二叉树展开为链表](https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/)

```java
 private TreeNode pre = null;

    public void flatten(TreeNode root) {
        preOrder(root);
    }

    private void preOrder(TreeNode root) {

        if (root == null) {
            return;
        }
        //需要提前把左右子树存储好
        TreeNode left = root.left;
        TreeNode right = root.right;
        if (pre == null) {
            pre = root;
        }else{
            pre.right = root;
            pre = root;
        }
        root.left = null;
        preOrder(left);
        preOrder(right);

    }
```

#### [48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/)

```java
 public void rotate(int[][] matrix) {
        for (int i = 0; i < matrix.length; i++) {
            for (int j = i + 1; j < matrix.length; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix.length / 2; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[i][matrix.length - 1 - j];
                matrix[i][matrix.length - 1 - j] = temp;
            }
        }
    }
```

#### @连续子串=连续子序列!=子序列 ;

**子串可能用滑动窗口或者梯度**

**子序列可能是背包.但是也不绝对**

#### @@和0为的连续最长子序列(子串,和为k)

@**利用求和的梯度来做.**

```
{3,0,-1,-2,-3,1,1,1,2,3,1,-2,-1}   12

{1，-1，1，-1，1，-1，1，-1}     8
```

```java
 public int func(int[] nums) {
        int maxLen = 0;
        int sum = 0;

        HashMap<Integer, Integer> map = new HashMap<>();
        map.put(0, 0);
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            if (map.containsKey(sum)) {
                int len = i+1 - map.get(sum);
                maxLen = Math.max(len, maxLen);
            } else {
                map.put(sum, i+1);
            }
        }
        return maxLen;
    }
```

#### @@最大长度的01连续子序列

给定一个数组，数组中只包含0和1。请找到一个最长的子序列，其中0和1的数量是相同的

因为01数量要相同,可以把0转换成-1, 变成上面一道题"和为0的连续最长子序列"

而上一道题的做法就是利用梯度+hashmap存梯度索引

```java
 public int getMaxLen(int[] nums) {

        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == 0) {
                nums[i] = -1;
            }
        }
        int maxLen = 0;
        HashMap<Integer, Integer> map = new HashMap<>();
        int sum = 0;
        map.put(0, -1);
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            nums[i] = sum;
            if (map.containsKey(nums[i])) {
                int len = i - map.get(nums[i]);
                maxLen = Math.max(len, maxLen);
            } else {
                map.put(sum, i);
            }
        }
        return maxLen;
    }
```

#### [全排列](https://leetcode-cn.com/problems/permutations/)

```java
List<List<Integer>> ret = new ArrayList<>();
    boolean[] visited = null;
    int len = 0;
    ArrayList<Integer> path = new ArrayList<Integer>();

    public List<List<Integer>> permute(int[] nums) {
        if (nums == null || nums.length == 0) {
            return ret;
        }
        len = nums.length;
        visited = new boolean[len];
        for (int i = 0; i < len; i++) {
            path.add(nums[i]);
            visited[i] = true;
            backtracing(nums, i, path);
            visited[i] = false;
            path.remove(path.size() - 1);
        }
        return ret;

    }

    private void backtracing(int[] nums, int index, ArrayList<Integer> path) {

        if (path.size() == len) {
            ret.add(new ArrayList<Integer>(path));
            return;
        }
        for (int i = 0; i < len; i++) {
            if (visited[i] == false) {
                path.add(nums[i]);
                visited[i] = true;
                backtracing(nums, i, path);
                visited[i] = false;
                path.remove(path.size() - 1);
            }
        }
    }
```

#### 012组合数

一道是长度为N的数组，每个元素为0或1或2，找到满足a\[i\]<a\[j\]<a\[k\]的\(i, j, k\)的数量

```java
 public int func(int[] nums) {
        int[][] remember = new int[nums.length][2];
        int countZero = 0;
        for (int i = 0; i < nums.length; i++) {
            if (i == 0) {
                remember[i][0] = 0;
            } else {
                remember[i][0] = countZero;
            }

            if (nums[i] == 0) {
                countZero++;
            }
        }
        int countTwo = 0;
        for (int i = nums.length - 1; i >= 0; i--) {
            if (i == nums.length - 1) {
                remember[i][1] = 0;
            } else {
                remember[i][1] = countTwo;
            }

            if (nums[i] == 2) {
                countTwo++;
            }
        }
        int ret = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == 1) {
                ret += remember[i][0] * remember[i][1];
            }
        }
        return ret;
    }
```

#### 求1到1000000的质数

```
先假设所有数都是素数,如果4不是素数,则 4的所有倍数都不是素数.
不需要再写判断一个数是否是素数的函数.因为比如比7小的数无法将7排除,则7一定是素数.
```

#### 三角形

```
判断三个数能否形成三角形，要求实现isTriangle\(int a, int b, int c\)

只能判断相减是否小于，不能用两边之和大于第三边，会溢出
```

####   链表相交的节点

```
断两个链表是否相交是求链表是否存在环的变式，如给定两个链表A和B

* A遍历自己，一直到终点再从B开始。B也如此，遍历完自己再遍历A。
* 如果中间有相同节点了，那么就相交，否则不相交。
* 因为如果相交的话，那么都会走到len\(A\)+len\(B\)-len\(A∩B\)的地方停下来
```

#### [@@@142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/)

https://leetcode.com/problems/linked-list-cycle-ii/submissions/

@第一步  先  快指针 2个  慢指针1个  再 判断是否相遇

@第二步 快指针和慢指针都1个    在判断是否相遇.

@ if (fast != null && fast == slow)     fast==slow  == null  是一种没有环的情况,此时fast没必要赋值为head了

```java
public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }

        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && slow != null) {
            fast = fast.next;
            if (fast != null) {
                fast = fast.next;
            }
            slow = slow.next;
            //fast判空
            if (fast != null && fast == slow) {
                fast = head;
                break;
            }
        }

        while (fast != null && slow != null) {
            if (fast == slow) {
                return fast;
            }
            //第二次循环是每人走一步.
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }
```

#### @@相亲数

amicable number, 两个正整数中，彼此的全部正约数之和与另一方相等

* 约数又叫因数，b是a的约数，则a%b==0，整除没有余数
* 220的所有因子\(除了自己以外的因子\)：1+2+4+5+10+11+20+22+44+55+110 = 284
* 284的所有因子：1+2+4+71+142 = 220

给出maxNum,求出0~maxNum的相亲数

@约的求解1.边界是`i*i<=num   2.i*i==num的时候只算一个i    `

@相亲数的寻找  i= 220  sum[i] = 284  sum[284] = 220  所以  sum[sum[i]] = i. 并且要注意 i= 6; sum[6]=6这种特殊情况

```java
 private int getSum(int num) {
        int sum = 0;
        for (int i = 1; i * i <= num; i++) {
            //i的三种特殊情况  假设num为100, i=10的时候只算一个10
            //i=1的时候  不加 100/1
            //其他时候 如果num%i==0   比如 i=2   则 2和50都加到sum里
            if (i * i == num) {
                return sum + i;
            }
            if (i == 1) {
                sum += 1;
                continue;
            }
            if (num % i == 0) {
                sum += i;
                sum += num / i;
            }
        }
        return sum;
    }

    public ArrayList<Integer> getAllNum(int maxNum) {
        ArrayList<Integer> ret = new ArrayList<>();
        int[] sum = new int[maxNum + 1];
        for (int i = 2; i <= maxNum; i++) {
            sum[i] = getSum(i);
        }
        for (int i = 2; i <= maxNum; i++) {
            if (sum[i] > maxNum) {
                continue;
            }
            if (sum[i] != i && sum[sum[i]] == i) {
                ret.add(i);
            }
        }
        return ret;
    }
```

#### @@@@@@@@链表快排.

```java
 public void listQuickSort(ListNode pHead) {
        quickSort(pHead, null);
    }

    public void quickSort(ListNode pHead, ListNode pEnd) {
        if (pHead == null || pHead == pEnd) {
            return;
        }
        ListNode mid = partition(pHead, pEnd);
        quickSort(pHead, mid);
        quickSort(mid.next, null);
    }

    private ListNode partition(ListNode start, ListNode end) {
        if (start == null) {
            return null;
        }
        if (start.next == end) {
            return start;
        }
        ListNode preOfSwapNum = start;
        int key = start.val;
        ListNode node = start.next;
        while (node != end) {
            if (node.val <= key) {
                preOfSwapNum = preOfSwapNum.next;
                swap(preOfSwapNum, node);
            }
            node = node.next;
        }
        swap(preOfSwapNum, start);
        return preOfSwapNum;
    }

    private void swap(ListNode p1, ListNode p2) {
        int temp = p1.val;
        p1.val = p2.val;
        p2.val = temp;
    }

    private ListNode forTestInitialNodeList(int[] nums) {
        ListNode pHead = new ListNode(-1);
        ListNode node = pHead;
        for (int temp : nums) {
            node.next = new ListNode(temp);
            node = node.next;
        }
        return pHead.next;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        ListNode node = solution.forTestInitialNodeList(new int[]{2, 1});
        solution.listQuickSort(node);
        while (node != null) {
            System.out.println(node.val);
            node = node.next;
        }
    }
```

#### [@@@@@148. 排序链表](https://leetcode-cn.com/problems/sort-list/)

@归并排序,需要截断list

@快速排序

```java
归并排序:
public ListNode sortList(ListNode head) {
        int len = 0;
        ListNode temp = head;
        while (head != null) {
            head = head.next;
            len++;
        }
        return mergeSort(temp, null, len);
    }

    private ListNode mergeSort(ListNode begin, ListNode end, int len) {
        if (begin == null || begin == end) {
            return begin;
        }

        int mid = len / 2;
        ListNode midNode = begin;
        int cnt = 1;
        while (cnt < mid) {
            cnt++;
            midNode = midNode.next;
        }

        ListNode begin2 = midNode.next;
        //记得把midNode截断这样l1和l2才是两段独立的链表.
        midNode.next = null;
        if (end != null) {
            end.next = null;
        }
        ListNode l1 = mergeSort(begin, midNode, mid);
        ListNode l2 = mergeSort(begin2, end, mid);
        return merge(l1, l2);
    }

    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode head = new ListNode(-1);
        ListNode node = head;
        if (l1 != null && l2 != null) {
            while (l1 != null && l2 != null) {
                if (l1.val <= l2.val) {
                    node.next = l1;
                    l1 = l1.next;
                } else {
                    node.next = l2;
                    l2 = l2.next;
                }
                node = node.next;
            }
            while (l1 != null) {
                node.next = l1;
                l1 = l1.next;
                node = node.next;
            }
            while (l2 != null) {
                node.next = l2;
                l2 = l2.next;
                node = node.next;
            }
            return head.next;
        } else if (l2 != null) {
            return l2;
        } else {
            return l1;
        }
    }

快速排序:

public ListNode sortList(ListNode head) {
        quickSort(head, null);
        return head;
    }

    private void quickSort(ListNode begin, ListNode end) {
        if (begin == null || begin == end) {
            return;
        }
        ListNode midNode = partition(begin, end);
        quickSort(begin, midNode);
        quickSort(midNode.next, end);
    }

    private ListNode partition(ListNode begin, ListNode end) {
        if (begin == null || begin == end) {
            return begin;
        }
        ListNode itsNextIsRightNum = begin;
        int key = begin.val;
        ListNode node = begin.next;
        while (node != end) {
            if (node.val <= key) {
                itsNextIsRightNum = itsNextIsRightNum.next;
                swap(itsNextIsRightNum, node);
            }
            node = node.next;
        }
        swap(itsNextIsRightNum, begin);
        return itsNextIsRightNum;
    }

    private void swap(ListNode p1, ListNode p2) {
        int temp = p1.val;
        p1.val = p2.val;
        p2.val = temp;
    }
```

#### [@@@表达式求值](https://www.nowcoder.com/questionTerminal/3e483fe3c0bb447bb17ffb3eeeca78ba?f=discussion)

```

今天上课，老师教了小易怎么计算加法和乘法，乘法的优先级大于加法，但是如果一个运算加了括号，那么它的优先级是最高的。例如：
1+2*3=7
1*(2+3)=5
1*2*3=6
(1+2)*3=9
现在小易希望你帮他计算给定3个数a，b，c，在它们中间添加"+"， "*"， "("， ")"符号，能够获得的最大值。

输入描述:
一行三个数a，b，c (1 <= a, b, c <= 10)

输出描述:
能够获得的最大值
```

```java

class Solution {
    private int getMax(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int len = nums.length;
        int[][] dp = new int[len][len];
        for (int l = 1; l <= len; l++) {
            for (int i = 0; i < len; i++) {
                int j = i + l - 1;
                if (j >= len) {
                    break;
                }
                if (i == j) {
                    dp[i][j] = nums[i];
                }
                for (int k = i; k < j; k++) {
                    int tempMax = Math.max(dp[i][k] + dp[k + 1][j], dp[i][k] * dp[k + 1][j]);
                    dp[i][j] = Math.max(tempMax, dp[i][j]);
                }
            }
        }
        return dp[0][len - 1];
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = scanner.nextInt();
        }
        Solution solution = new Solution();
        int max = solution.getMax(nums);
        System.out.println(max);
    }
}
```

<file_sep>/notes/秒杀项目.md

<!-- TOC -->

- [秒杀项目的设计要点](#秒杀项目的设计要点)
- [项目功能介绍](#项目功能介绍)
- [redis 集群搭建](#redis-集群搭建)
- [15.3.1. 分布式锁实现源码](#1531-分布式锁实现源码)
        - [15.3.3. Lua脚本实现分布式锁](#1533-lua脚本实现分布式锁)
        - [15.3.4. RedisConnection实现分布式锁](#1534-redisconnection实现分布式锁)
- [17. redis排行榜](#17-redis排行榜)
    - [17.1. 项目中实现的功能](#171-项目中实现的功能)
    - [17.2. 功能设计](#172-功能设计)
- [18. redis秒杀系统](#18-redis秒杀系统)
        - [18.0.1. 秒杀系统需求分析](#1801-秒杀系统需求分析)
        - [18.0.2. 秒杀系统功能步骤梳理](#1802-秒杀系统功能步骤梳理)
        - [18.0.3. 秒杀系统功能api实战(上)](#1803-秒杀系统功能api实战上)
        - [18.0.4. 秒杀系统功能api实战(中)](#1804-秒杀系统功能api实战中)
        - [18.0.5. 秒杀系统功能api实战(下)](#1805-秒杀系统功能api实战下)

<!-- /TOC -->
# 秒杀项目的设计要点
设计思路：将请求拦截在系统上游，降低下游压力。在一个并发量大，实际需求小的系统中，应当尽量在前端拦截无效流量，降低下游服务器和数据库的压力，不然很可能造成数据库读写锁冲突，甚至导致死锁，最终请求超时。

限流：前端直接限流，允许少部分流量流向后端

削峰：瞬时大流量峰值容易压垮系统，解决这个问题是重中之重。常用的消峰方法有异步处理、缓存和消息中间件等技术。

异步处理：秒杀系统是一个高并发系统，采用异步处理模式可以极大地提高系统并发量，其实异步处理就是削峰的一种实现方式。

内存缓存：秒杀系统最大的瓶颈一般都是数据库读写，由于数据库读写属于磁盘IO，性能很低，如果能够把部分数据或业务逻辑转移到内存缓存，效率会有极大地提升。

消息队列：消息队列可以削峰，将拦截大量并发请求，这也是一个异步处理过程，后台业务根据自己的处理能力，从消息队列中主动的拉取请求消息进行业务处理。

# 项目功能介绍

uid发送红包,保证uid存在,雪花算法生成红包id与其他红包相关信息,用mq异步存入数据库,把核心信息存入redis缓存

uid抢红包  判断用户是否存在,首先经过redis抢到红包,如果红包抢完,数量为0,就直接返回,如果没抢完就数量减一,金额随机,但是要保证最后一个红包把钱花完.同时把红包记录异步入库.

查看红包流水记录,数据库的两张表,一张是发红包记录,一张是抢红包记录

秒杀:

(改进点,mq异步入库,uid的判断,抢红包的时候可以用并发,连接可以使用netty,可以把整个微服务部署成高可用的状态)

因为redis是集群,假设我们要在下午四点开始秒杀,但是不同服务器时间上可能会有偏差.如果他们不能在同一时间点对用户提供服务,会造成流量倾斜.  所以可以设置一个时间标志位,  是否开始_开始时间.  开始之后设为1.

然后就允许秒杀用户进入,同时秒杀用户的数量是秒杀商品数量的1.2倍,为了保证进来的用户减少无效用户和重复用户.

如果抢购用户大于允许进入的数量,就返回给其他用户请购人数已满,其他用户参与抢购.抢购完成之后把id存入布隆过滤器,如果过滤器中已经存在则过滤掉,因为他已经参与过抢购.其他人继续抢购,并把抢购记录异步入库.

sql 语句

mysql  主从备份.binlog, io 与sql进程.  读写分离用代理实现,mycat

文档管理微服务
前端把文件实体传给我,  我根据调用document的函数来把文件存到正确的位置上.

 跨四库数据生成 XML 微服务.  难点就是spring datajpa连接四个库的配置,连接好之后,就是读数据,然后自己在四个库之间做join,  把join得到的数据装进类里,然后生成想要的格式的xml文件.

# redis 集群搭建
- 安装redis

  * 处理步骤

  ```shell
  cd /usr/local/
  wget http://download.redis.io/releases/redis-4.0.6.tar.gz
  tar -zxvf redis-4.0.6.tar.gz
  cd redis-4.0.6
  make && make install
  ```

- 新建集群文件夹

  - 处理步骤

    ```shell
    cd /usr/local/
    mkdir redis_cluster
    cd redis_cluster
    mkdir 7000 7001 7002 7003 7004 7005
    cp /usr/local/redis-4.0.6/redis.conf  /usr/local/redis_cluster/7000
    ```

- 修改redis_cluster/7000到redis_cluster/7005文件夹下面的Redis.conf

  *  处理步骤

```shell
daemonize    yes                          //redis后台运行
port  7000                                //端口7000,7002,7003
cluster-enabled  yes                      //开启集群  把注释#去掉
cluster-config-file  nodes.conf      //集群的配置  配置文件首次启动自动生成 7000,7001,7002
cluster-node-timeout  5000                //请求超时  设置5秒够了
appendonly  yes                           //aof日志开启  有需要就开启，它会每次写操作都记录一条日志
bind 127.0.0.1 172.16.244.144(此处为自己内网的ip地址，centos7下面采用ip addr来查看，其他系统试一下ifconfig查看，ip为)

```

- 在其他节点也修改完Redis.conf

  * 处理步骤

  ```shell
  cp /usr/local/redis_cluster/7000/redis.conf /usr/local/redis_cluster/7001
  cp /usr/local/redis_cluster/7000/redis.conf /usr/local/redis_cluster/7002
  cp /usr/local/redis_cluster/7000/redis.conf /usr/local/redis_cluster/7003
  cp /usr/local/redis_cluster/7000/redis.conf /usr/local/redis_cluster/7004
  cp /usr/local/redis_cluster/7000/redis.conf /usr/local/redis_cluster/7005

  ```

- 启动所有redis节点cd redis-server所在的路径

    * 处理步骤

    ```shell
    cp /usr/local/redis-4.0.6/src/redis-server /usr/local/ redis-cluster

    cd /usr/local/redis_cluster/7000 ../redis-server ./redis.conf

    cd /usr/local/redis-cluster/7001 ../redis-server ./redis.conf

    cd /usr/local/redis-cluster/7002 ../redis-server ./redis.conf

    cd /usr/local/redis-cluster/7003 ../redis-server ./redis.conf

    cd /usr/local/redis-cluster/7004 ../redis-server ./redis.conf

    cd /usr/local/redis-cluster/7005 ../redis-server ./redis.conf
    ```

- 创建集群

  * 前面已经准备好了搭建集群的redis节点，接下来我们要把这些节点都串连起来搭建集群。官方提供了一个工具：redis-trib.rb(/usr/local/redis-4.0.6/src/redis-trib.rb) 看后缀就知道不能直接执 行，它是用ruby写的一个程序，所以我们还得安装ruby.

  ```shell
    yum -y install ruby ruby-devel rubygems rpm-build

    gem install redis
  ```

- 如果gem install redis发现报错

    ```shell
    curl -L get.rvm.io | bash -s stable

    source /usr/local/rvm/scripts/rvm

    rvm list known

    rvm install 2.3.3

    rvm use 2.3.3

    ruby --version

    gem install redis
    ```

- 开启集群工作

     ```shell
     cd /usr/local/redis-4.0.6/src

     设置集群的ip要设置局域网ip而不是本机,不然springboot在连接集群的时候,如果springboot主机跟集群不在同一个局域网内,就会导致springboot去127.0.0.1找集群却找不到

    (不推荐) ./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \
           127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
     (推荐) ./redis-trib.rb create --replicas 1 192.168.245.5:7000 192.168.245.5:7001 \
           192.168.245.5:7002 192.168.245.5:7003 192.168.245.5:7004 192.168.245.5:7005
     ```

- 测试集群是否正常

     ```
     ./redis-cli -c -p 7000
     ```

- 如果搭建失败，请用此命令将所有启动的redis server一个个关闭掉

     ```
     ./redis-cli -p 7000 shutdown
     ```

# 15.3.1. 分布式锁实现源码

* 分布锁满足两个条件，一个是加有效时间的锁，一个是高性能解锁

* 采用redis命令setnx（set if not exist）、setex（set expire value）实现

* 【千万记住】解锁流程不能遗漏，否则导致任务执行一次就永不过期

* 将加锁代码和任务逻辑放在try，catch代码块，将解锁流程放在finally

* 通过scp方式将项目打包jar包放到集群里面的服务器

* 通过nohup启动jar包

### 15.3.3. Lua脚本实现分布式锁

**简介：手把手进行Lua脚本讲解和setnx、setex命令连用讲解**

*  Lua简介
  * 从 Redis 2.6.0 版本开始，通过内置的 Lua 解释器，可以使用 EVAL 命令对 Lua 脚本进行求值。
  * Redis 使用单个 Lua 解释器去运行所有脚本，并且， Redis 也保证脚本会以原子性(atomic)的方式执行：当某个脚本正在运行的时候，不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来，脚本的效果(effect)要么是不可见的(not visible)，要么就是已完成的(already completed)。

* Lua脚本配置流程
  *  1、在resource目录下面新增一个后缀名为.lua结尾的文件
  * 2、编写lua脚本
  * 3、传入lua脚本的key和arg
  * 4、调用redisTemplate.execute方法执行脚本
* Lua脚本结合RedisTempalte实战演练
* Lua脚本其他工作场景剖析和演练
* lua eval http://doc.redisfans.com/script/eval.html

### 15.3.4. RedisConnection实现分布式锁

RedisConnection实现分布锁的方式，采用redisTemplate操作redisConnection
           实现setnx和setex两个命令连用**

- redisTemplate本身有没通过valueOperation实现分布式锁

  * 问题探索：
             Spring Data Redis提供了与Java客户端包的集成服务，比如Jedis, JRedis等
             通过getNativeConnection的方式可以解决问题吗？

- Spring Data Redis提供了与Java客户端包的集成服务，比如Jedis, JRedis等

  * 代码演示

  * ```java
    /**
            * 重写redisTemplate的set方法
            *

            * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
            *

            * 客户端执行以上的命令：
            *

            * 如果服务器返回 OK ，那么这个客户端获得锁。
            * 如果服务器返回 NIL ，那么客户端获取锁失败，可以在稍后再重试。
            *
            * @param key     锁的Key
            * @param value   锁里面的值
            * @param seconds 过去时间（秒）
            * @return
         */
          private String set(final String key, final String value, final long seconds) {
            Assert.isTrue(!StringUtils.isEmpty(key), "key不能为空");
            return redisTemplate.execute(new RedisCallback<String>() {
                @Override
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    String result = null;
                    if (nativeConnection instanceof JedisCommands) {
                        result = ((JedisCommands) nativeConnection).set(key, value, NX, EX, seconds);
                    }

                    if (!StringUtils.isEmpty(lockKeyLog) && !StringUtils.isEmpty(result)) {
                        logger.info("获取锁{}的时间：{}", lockKeyLog, System.currentTimeMillis());
                    }

                    return result;
                }
            });
          }
    ```

- 为什么新版本的spring-data-redis会报class not can not be case错误

  ```
  io.lettuce.core.RedisAsyncCommandsImpl cannot be cast to redis.clients.jedis.JedisCommands
  ```

- 探索spring-data-redis升级

  * 官网api分析
             https://docs.spring.io/spring-data/redis/docs/1.5.0.RELEASE/api/
             https://docs.spring.io/spring-data/redis/docs/2.0.13.RELEASE/api/

  * 源码改造

    ```java
    public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
          RedisConnection redisConnection =             redisTemplate.getConnectionFactory().getConnection();
         return redisConnection.set(key.getBytes(), getHostIp().getBytes(), Expiration.seconds(expire), RedisStringCommands.SetOption.ifAbsent());
    }

    ```

# 17. redis排行榜

## 17.1. 项目中实现的功能

1、用sorted Set实现过排行榜项目

2、用过期key结合springboot cache实现过缓存存储

3、redis实现分布式环境seesion共享

4、用布隆过滤器解决过缓存穿透

5、redis实现分布式锁

6、redis实现订单重推系统

## 17.2. 功能设计

    1、ZSetOperations重要api讲解（sortedSet）

        1、排行榜：
             排行榜功能是一个很普遍的需求。使用 Redis 中有序集合的特性来实现排行榜是又好又快的选择。
             一般排行榜都是有实效性的，比如“用户积分榜”，游戏中活跃度排行榜，游戏装备排行榜等。

          面临问题：
             数据库设计复杂，并发数较高，数据要求实时性高

        2、redis实现排行榜api讲解

    2、浅谈mysql数据库表设计过程中几个关键要点

        简介：数据库表score_flow（积分流水表）、user_score（用户积分表总表）设计，用于：1）查top100  2）查用户的排名

        1、表设计过程中应该注意的点即数据类型
              1）更小的通常更好
                  控制字节长度

              2）使用合适的数据类型
                  如tinyint只占8个位，char(1024)与varchar(1024)的对比,char用于类似定长数据存储比varchar节省空间，如：uuid（32），可以用char(32).

              3）尽量避免NULL建议使用NOT NULL DEFAULT ''
                  NULL的列会让索引统计和值比较都更复杂。可为NULL的列会占据更多的磁盘空间，在Mysql中也需要更多复杂的处理程序

        2、索引设计过程中应该注意的点

              1）选择唯一性索引
                  唯一性索引的值是唯一的，可以更快速的通过该索引来确定某条记录,保证物理上面唯一

              2）为经常需要排序、分组和联合操作的字段建立索引
                  经常需要ORDER BY、GROUP BY、DISTINCT和UNION等操作的字段，排序操作会浪费很多时间

              3)常作为查询条件的字段建立索引
                  如果某个字段经常用来做查询条件，那么该字段的查询速度会影响整个表的查询速度

              4)数据少的地方不必建立索引

  3、sql优化，explain查看执行计划(注意：扫描行数会影响CPU运行，占用大量内存)
        简介：sql优化以及mybatis generator反向工程讲解

              1） 能够用BETWEEN的就不要用IN

              2） 能够用DISTINCT的就不用GROUP BY

              1)  避免数据类型强转

              4） 学会采用explain查看执行计划

        续：org.mybatis.generator配置讲解

        引入：
        <dependency>
      <groupId>org.mybatis.generator</groupId>
      <artifactId>mybatis-generator-core</artifactId>
      <scope>test</scope>
      <version>1.3.2</version>
      <optional>true</optional>
    </dependency>

    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.5</version>
    </dependency>

4、排行榜三大接口讲解

         1、添加用户积分

         2、获取top N 排行
             redisService新增方法reverseRangeWithScores()

         3、根据用户ID获取排行
             zset.rank(key,value)，key为set的名称，value为用户id

    5、springboot项目初始化加载讲解
         场景：将一千万用户白名单load缓存，用户请求的时候判断该用户是否是缓存里面的用户

         1、springboot实现初始化加载配置（实现缓存预热）

             1、采用实现springboot ApplicationRunner
                该方法仅在SpringApplication.run(…)完成之前调用

             2、采用实现InitializingBean
                InitializingBean接口为bean提供了初始化方法的方式，它只包括afterPropertiesSet()方法。
                在spring初始化bean的时候，如果bean实现了InitializingBean接口，
                在对象的所有属性被初始化后之后才会调用afterPropertiesSet()方法

         2、初始化同步redis数据

         3、初始化完成再放入请求

# 18. redis秒杀系统

### 18.0.1. 秒杀系统需求分析

**简介：采用大厂需求分析步骤对秒杀需求功能分析**

- 功能核心点

  * 经典互联网商品抢购秒杀功能

- 功能api

  * 商品秒杀接口

- 数据落地存储方案

  * 通过分布式redis减库存
  * DB存最终订单信息数据

- api性能调优

  * 性能瓶颈在高并发秒杀
  * 技术难题在于超卖问题

### 18.0.2. 秒杀系统功能步骤梳理

**简介：后端秒杀功能步骤梳理 **

- 利用 Redis 缓存incr拦截流量

  - 首先通过数据控制模块，提前将秒杀商品缓存到读写分离 Redis，并设置秒杀开始标记如下：

    ```shell
    "skuId_start": 0   //开始标记0表示秒杀开始
    "skuId_count": 10000 //总数
    "skuId_access": 12000  //接受抢购数
    ```

  - 秒杀开始前，服务集群读取 goodsId_Start 为 0，直接返回未开始。

  - **服务时间不一致可能导致流量倾斜,所以要有开始标记**

  - 数据控制模块将 goodsId_start 改为1，标志秒杀开始。

  - 当接受下单数达到 sku_count*1.2 后，继续拦截所有请求，商品剩余数量为 0

- 利用Redis缓存加速库存扣量

  ```shell
  "skuId_booked": 10000 //总数0开始10000  通过incr扣减库存，返回抢购成功
  ```

- 将用户订单数据写入mq

- 监听mq入库

### 18.0.3. 秒杀系统功能api实战(上)

**简介：后端秒杀网关流量拦截层功能开发 **

- 先判断秒杀是否已经开始
  * 初始化时将key  SECKILL_START_1 value 0_1554046102存入数据库中
- 利用 Redis 缓存incr拦截流量
  - 缓存拦截流量代码编写
  - 用incr方法原子加
  - 通过原子加判断当前skuId_access是否达到最大值
  - 思考：是否需要保证获取到值的时候和incr值两个命令的原子性
    * 保证原子性的方式，采用lua脚本
    * 采用lua脚本方式保证原子性带来缺点，性能有所下降
    * 不保证原子性缺点，放入请求量可能大于skuId_access

### 18.0.4. 秒杀系统功能api实战(中)

**简介：后端秒杀信息校验层功能开发布隆过滤器实现重复购买拦截 **

- 订单信息校验层

  * 校验当前用户是否已经买过这个商品
    - 需要存储用户的uid
    - 存数据库效率太低
    - 存Redis value方式数据太大
    - 存布隆过滤器性能高且数据量小

- 校验完通过直接返回抢购成功

### 18.0.5. 秒杀系统功能api实战(下)

  **简介：后端秒杀信息校验层功能开发lua脚本实现库存扣除**

  - 库存扣除成功，获取当前最新库存

  - 如果库存大于0，即马上进行库存扣除，并且访问抢购成功给用户

  - 考虑原子性问题

    * 保证原子性的方式，采用lua脚本
    * 采用lua脚本方式保证原子性带来缺点，性能有所下降
    * 不保证原子性缺点，放入请求量可能大于预期值
    * 当前扣除库存场景必须保证原子性，否则会导致超卖

  - 返回抢购结果

    * 抢购成功
    * 库存没了 ，抢购失败

<file_sep>/notes/计算机网络.md
<!-- TOC -->

- [UDP 和 TCP 的特点](#udp-%e5%92%8c-tcp-%e7%9a%84%e7%89%b9%e7%82%b9)
- [UDP 首部格式](#udp-%e9%a6%96%e9%83%a8%e6%a0%bc%e5%bc%8f)
- [TCP 首部格式](#tcp-%e9%a6%96%e9%83%a8%e6%a0%bc%e5%bc%8f)
- [TCP 的三次握手](#tcp-%e7%9a%84%e4%b8%89%e6%ac%a1%e6%8f%a1%e6%89%8b)
- [TCP 的四次挥手](#tcp-%e7%9a%84%e5%9b%9b%e6%ac%a1%e6%8c%a5%e6%89%8b)
- [TCP 可靠传输](#tcp-%e5%8f%af%e9%9d%a0%e4%bc%a0%e8%be%93)
- [TCP 滑动窗口](#tcp-%e6%bb%91%e5%8a%a8%e7%aa%97%e5%8f%a3)
- [TCP 流量控制](#tcp-%e6%b5%81%e9%87%8f%e6%8e%a7%e5%88%b6)
- [TCP 拥塞控制](#tcp-%e6%8b%a5%e5%a1%9e%e6%8e%a7%e5%88%b6)
  - [1. 慢开始与拥塞避免](#1-%e6%85%a2%e5%bc%80%e5%a7%8b%e4%b8%8e%e6%8b%a5%e5%a1%9e%e9%81%bf%e5%85%8d)
  - [2. 快重传与快恢复](#2-%e5%bf%ab%e9%87%8d%e4%bc%a0%e4%b8%8e%e5%bf%ab%e6%81%a2%e5%a4%8d)
    - [15.0.2. TCP三次握手](#1502-tcp%e4%b8%89%e6%ac%a1%e6%8f%a1%e6%89%8b)
    - [15.0.3. TCP四次挥手](#1503-tcp%e5%9b%9b%e6%ac%a1%e6%8c%a5%e6%89%8b)
- [127.0.0.1和0.0.0.0地址的区别](#127001%e5%92%8c0000%e5%9c%b0%e5%9d%80%e7%9a%84%e5%8c%ba%e5%88%ab)
  - [0.0.0.0](#0000)
    - [用途总结](#%e7%94%a8%e9%80%94%e6%80%bb%e7%bb%93)
  - [127.0.0.1](#127001)
    - [用途](#%e7%94%a8%e9%80%94)
  - [总结](#%e6%80%bb%e7%bb%93)

<!-- /TOC -->

网络层只把分组发送到目的主机，但是真正通信的并不是主机而是主机中的进程。传输层提供了进程间的逻辑通信，传输层向高层用户屏蔽了下面网络层的核心细节，使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑通信信道。

# 常见协议

<img src="pictures/计算机网络/image-20200412173140775.png" alt="image-20200412173140775" style="zoom:50%;" />

# Ethernet协议

以太网是目前最流行的一种局域网组网技术（其他常见局域网组网技术还有令牌环局域网、无线局域网、ATM局域网），以太网技术的正式标准是IEEE 802.3标准，它规定了在以太网中传输的数据帧结构，如下图所示。

<img src="pictures/计算机网络/image-20200412174242182.png" alt="image-20200412174242182" style="zoom:50%;" />

在物理层上看，一个完整的以太网帧有7个字段，事实上，前两个字段并不能算是真正意义上的以太网数据帧，它们是以太网在物理层上发送以太网数据时添加上去的。为了实现底层数据的正确阐述，物理层使用7个字节前同步码（0和1交替的56位（55-55-55-55-55-55-55））实现物理层帧输入/输出同步；使用1个字节的SFD（帧首定界符，固定为10101011）标识帧的开始。上图中剩下的5个字段是真正的以太网数据，其中包含了目的地址和源地址，它们都是6字节长度（通常每个网卡都有1个6个字节MAC地址，以在以太网中唯一地标识自己）。网卡接收数据时，通过将目的地址字段和自身的MAC地址做比较，判断是否接收该数据包。通常，将这里的6字节目的地址按照下面的格式来书写，如：00-01-02-03-04-05。这6个字节在以太网中是按照从左到右的顺序发送的，同时对每个字节来说，最先发送的是最低位bit0，最后是最高位bit7。

      在以太网帧中，目的地址可以分为三类：单播地址、多播地址和广播地址。单播地址通常与一个具体网卡的MAC地址相对应，它要求第一个字节的bit0（即最先发出去的位）必须是0；多播地址则要求第一个字节的bit0为1，这样，在网络中多播地址不会与任何网卡的MAC相同，多播数据可以被很多个网卡同时接收；广播地址的所有48位全为1（即FF-FF-FF-FF-FF-FF），同一局域网中的所有网卡可以接收广播数据包。

上图中的长度/类型具有两个意义，当这两个字节的值小雨1518时，那么它就代表其后数据字段的长度；如果这两个字节的值大于1518，则表示该以太网帧中的数据属于哪个上层协议（例如0x800，代表IP数据包；0x806，代表ARP数据包等。）

在使用网卡进行数据包的发送与接收时，网卡已为我们完成了物理层的所有工作，驱动程序要做的是，在发送数据时，将目的地址、源地址、类型/长度、数据和填充这些值写入网卡，网卡自动计算其CRC并添加在数据帧尾部，同时对数据帧进行物理层的封装，最后将数据帧发送出去；在接收数据时，网卡会自动检测并接收数据包，验证校验和并把上述四个字段的值放在内部SRAM中供控制器读取。

TCP/IP协议有自己的地址：32bit的IP地址（网络地址），网络层发送数据包时只知道目的地址的IP地址，而底层接口（如以太网驱动程序）必须知道对方的硬件地址才能将数据发送出去。

# ARP协议

ARP的功能是在32bit的IP地址和采用不同网络技术的硬件地址之间提供动态映射，为上层将底层的物理地址差异屏蔽起来，这样上层的因特网协议就可以灵活地使用IP地址进行通信了。ARP协议的基本功能是使用目标主机的IP地址，查询其对应的MAC地址，以保证底层链路上数据包通信的进行。为了实现在网络接口物理地址与IP地址间的转换，ARP协议中引入了ARP缓存表的概念。ARP缓存表中记录了一条一条的<IP地址，MAC地址>对，他们是主机最近运行获得的关于周围其他主机的IP地址到物理地址的绑定，当需要发送IP数据包时，ARP层根据目的IP地址来查找ARP缓存表，并将匹配的MAC地址装入以太网帧首部，最后发送以太网数据。

ARP缓存表的建立与ARP数据包是密切相关的。在以太网中，ARP数据包和IP数据包是两个独立的部分，它们都封装在以太网帧中发送。ARP数据包的种类有两种：一是ARP请求包，它是通过以太网广播的方式发送的，用于向具有某个IP地址的主机发送请求，希望该主机返回其MAC地址；二是ARP应答包，收到ARP请求的主机会比对该数据包中的IP地址与自己的IP地址是否符合，若是，则该主机向源主机返回一个ARP应答包。向源主机报告自己的MAC地址。源主机通过提取ARP应答包中的相关字段来更新ARP缓存表。在Windows控制台上输入arp -a，可以查看操作系统中使用的ARP缓存表。

举一个简单的例子来看看ARP的功能。假如我们的主机（192.168.1.11）需要向开发板（192.168.1.37）发送一个IP数据包，当发送数据时，主机会在自己的ARP缓存表中寻找是否有目标IP地址。如果找到了，也就知道了目标MAC地址为（04-02-35-00-00-01），此时，主机直接把目标MAC地址写入以太网首部发送就可以了；如果在ARP缓存表中没有找到相对应的IP地址，此时比较不幸，我们的数据需要被延迟发送，随后主机会先在网络上发送一个广播（ARP请求，以太网目的地址为FF-FF-FF-FF-FF-FF），广播的ARP请求表示同一网段内所有主机将会收到这样一条信息：“192.168.1.37的MAC地址是什么？请回答”。网络IP地址为192.168.1.37（开发板）的主机接收到这个帧后，它有义务做出这样的回答（ARP应答）：“192.168.1.37的MAC地址是（04-02-35-00-00-01）”。这样，主机就知道了开发板的MAC地址，先前被延时的数据包就可以被发送了，此外，主机将这个地址对保存在缓存表中，以便后续数据包发送时使用。

ARP协议的核心就是对ARP缓存表的操作。发送数据包时，查找缓存表以得到目的MAC地址，此外，ARP还需要不断地处理ARP请求包和ARP应答包，以保证缓存表中各个表项的有效性。ARP的实质就是对缓存表的建立、更新、查询等操作。

## 2.2 ARP报文

要在源主机上建立关于目标主机的IP地址与MAC地址对应表项，则源主机和目的主机的基本信息交互式必须的，简单地说就是，源主机如何告诉目的主机：我需要你的MAC地址；而目的主机如何回复：这就是我的MAC地址。这时ARP报文（ARP数据包）就派上用场了。

ARP请求和ARP应答都是被组装在一个ARP数据包中发送的，ARP包的组成结构如下图所示。需要注意的是：ARP包时被封装在以太网帧中发送的，所以在图中也列出了以太网帧头部。

<img src="pictures/计算机网络/image-20200412174315178.png" alt="image-20200412174315178" style="zoom:50%;" />

  以太网帧头部中的前两个字段是以太网的MAC地址和源MAC地址，目的地址为全1的特殊地址是以太网广播地址。在ARP表项建立前，源主机只知道目的主机的IP地址，并不知道其MAC地址，所以在数据链路上，源主机只有通过广播的方式将ARP请求数据包发送出去，同一网段上的所有以太网接口都会接收到广播的数据包。

   两个字节长的以太网帧类型表示帧中数据的类型。对于ARP包来说，该字段值为0x0806；对IP包来说，该字段的值为0x0800。接下来就是ARP数据包部分了，第一个硬件类型字段表示发送方想要知道的硬件接口类型，对于以太网MAC地址，它的值为1.协议类型字段表示要映射的协议地址类型，它的值为0x0800时，即表示要映射为IP地址，该值与以太网数据帧头中的类型字段的值使用相同的一组值。

接下来的两个单字节长度的字段，称为硬件地址长度和协议地址长度，它们分别指出硬件地址和协议地址的长度，长度单位为字节。对于以太网上ARP请求或应答来说，它们的值分别为6和4，代表MAC地址的长度和IP地址的长度。在ARP协议包中流出硬件地址长度和协议地址长度字段可以使得ARP协议在任何网络中被使用，而不仅仅只在以太网中。

  操作字段op指出ARP数据包的类型，它们可以使ARP请求（值为1）、ARP应答（值为2）。  接下来的四个字段是发送端的以太网MAC地址、发送端的IP地址、目的端的以太网MAC地址和目的端的IP地址。

# UDP 和 TCP 的特点

- 用户数据报协议 UDP（User Datagram Protocol）是无连接的，尽最大可能交付，没有拥塞控制，面向报文（对于应用程序传下来的报文不合并也不拆分，只是添加 UDP 首部），支持一对一、一对多、多对一和多对多的交互通信。

- 传输控制协议 TCP（Transmission Control Protocol）是面向连接的，提供可靠交付，有流量控制，拥塞控制，提供全双工通信，面向字节流（把应用层传下来的报文看成字节流，把字节流组织成大小不等的数据块），每一条 TCP 连接只能是点对点的（一对一）。

# UDP 首部格式

<div align="center"> <img src="pics/d4c3a4a1-0846-46ec-9cc3-eaddfca71254.jpg" width="600"/> </div><br>

首部字段只有 8 个字节，包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。

# TCP 首部格式

<div align="center"> <img src="pics/55dc4e84-573d-4c13-a765-52ed1dd251f9.png" width="700"/> </div><br>

-  **序号**  ：用于对字节流进行编号，例如序号为 301，表示第一个字节的编号为 301，如果携带的数据长度为 100 字节，那么下一个报文段的序号应为 401。

-  **确认号**  ：期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段，序号为 501，携带的数据长度为 200 字节，因此 B 期望下一个报文段的序号为 701，B 发送给 A 的确认报文段中确认号就为 701。

-  **数据偏移**  ：指的是数据部分距离报文段起始处的偏移量，实际上指的是首部的长度。

-  **确认 ACK**  ：当 ACK=1 时确认号字段有效，否则无效。TCP 规定，在连接建立后所有传送的报文段都必须把 ACK 置 1。

-  **同步 SYN**  ：在连接建立时用来同步序号。当 SYN=1，ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接，则响应报文中 SYN=1，ACK=1。

-  **终止 FIN**  ：用来释放一个连接，当 FIN=1 时，表示此报文段的发送方的数据已发送完毕，并要求释放连接。

-  **窗口**  ：窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制，是因为接收方的数据缓存空间是有限的。

# TCP 的三次握手

<div align="center"> <img src="pics/e92d0ebc-7d46-413b-aec1-34a39602f787.png" width="600"/> </div><br>

假设 A 为客户端，B 为服务器端。

- 首先 B 处于 LISTEN（监听）状态，等待客户的连接请求。

- A 向 B 发送连接请求报文，SYN=1，ACK=0，选择一个初始的序号 x。

- B 收到连接请求报文，如果同意建立连接，则向 A 发送连接确认报文，SYN=1，ACK=1，确认号为 x+1，同时也选择一个初始的序号 y。

- A 收到 B 的连接确认报文后，还要向 B 发出确认，确认号为 y+1，序号为 x+1。

- B 收到 A 的确认后，连接建立。

**三次握手的原因** (两次握手弊端)

第三次握手是为了防止失效的连接请求到达服务器，让服务器错误打开连接。

客户端发送的连接请求如果在网络中滞留，那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后，就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器，如果不进行三次握手，那么服务器就会打开两个连接。如果有第三次握手，客户端会忽略服务器之后发送的对滞留连接请求的连接确认，不进行第三次握手，因此就不会再次打开连接。

# TCP 的四次挥手

<div align="center"> <img src="pics/f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg" width="600"/> </div><br>

以下描述不讨论序号和确认号，因为序号和确认号的规则比较简单。并且不讨论 ACK，因为 ACK 在连接建立之后都为 1。

- A 发送连接释放报文，FIN=1。

- B 收到之后发出确认，此时 TCP 属于半关闭状态，B 能向 A 发送数据但是 A 不能向 B 发送数据。

- 当 B 不再需要连接时，发送连接释放报文，FIN=1。

- A 收到后发出确认，进入 TIME-WAIT 状态，等待 2 MSL（最大报文存活时间）后释放连接。

- B 收到 A 的确认后释放连接。

**四次挥手的原因**

客户端发送了 FIN 连接释放报文之后，服务器收到了这个报文，就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据，传送完毕之后，服务器会发送 FIN 连接释放报文。

**TIME_WAIT**

客户端接收到服务器端的 FIN 报文后进入此状态，此时并不是直接进入 CLOSED 状态，还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由：

- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文，那么就会重新发送连接释放请求报文，A 等待一段时间就是为了处理这种情况的发生。

- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失，使得下一个新的连接不会出现旧的连接请求报文。

# TCP 可靠传输

TCP 使用超时重传来实现可靠传输：如果一个已经发送的报文段在超时时间内没有收到确认，那么就重传这个报文段。

一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT，加权平均往返时间 RTTs 计算如下：

<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTTs=(1-a)*(RTTs)+a*RTT" class="mathjax-pic"/></div> <br>

其中，0 ≤ a ＜ 1，RTTs 随着 a 的增加更容易受到 RTT 的影响。

超时时间 RTO 应该略大于 RTTs，TCP 使用的超时时间计算如下：

<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTO=RTTs+4*RTT_d" class="mathjax-pic"/></div> <br>

其中 RTT<sub>d</sub> 为偏差的加权平均值。

# TCP 滑动窗口

窗口是缓存的一部分，用来暂时存放字节流。发送方和接收方各有一个窗口，接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小，发送方根据这个值和其它信息设置自己的窗口大小。

发送窗口内的字节都允许被发送，接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认，那么就将发送窗口向右滑动一定距离，直到左部第一个字节不是已发送并且已确认的状态；接收窗口的滑动类似，接收窗口左部字节已经发送确认并交付主机，就向右滑动接收窗口。

接收窗口只会对窗口内最后一个按序到达的字节进行确认，例如接收窗口已经收到的字节为 {31, 34, 35}，其中 {31} 按序到达，而 {34, 35} 就不是，因此只对字节 31 进行确认。发送方得到一个字节的确认之后，就知道这个字节之前的所有字节都已经被接收。

<div align="center"> <img src="pics/a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg" width="800"/> </div><br>

# TCP 流量控制

流量控制是为了控制发送方发送速率，保证接收方来得及接收。

接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小，从而影响发送方的发送速率。将窗口字段设置为 0，则发送方不能发送数据。

# TCP 拥塞控制

如果网络出现拥塞，分组将会丢失，此时发送方会继续重传，从而导致网络拥塞程度更高。因此当出现拥塞时，应当控制发送方的速率。这一点和流量控制很像，但是出发点不同。流量控制是为了让接收方能来得及接收，而拥塞控制是为了降低整个网络的拥塞程度。

<div align="center"> <img src="pics/51e2ed95-65b8-4ae9-8af3-65602d452a25.jpg" width="500"/> </div><br>

TCP 主要通过四个算法来进行拥塞控制：慢开始、拥塞避免、快重传、快恢复。

发送方需要维护一个叫做拥塞窗口（cwnd）的状态变量，注意拥塞窗口与发送方窗口的区别：拥塞窗口只是一个状态变量，实际决定发送方能发送多少数据的是发送方窗口。

为了便于讨论，做如下假设：

- 接收方有足够大的接收缓存，因此不会发生流量控制；
- 虽然 TCP 的窗口基于字节，但是这里设窗口的大小单位为报文段。

<div align="center"> <img src="pics/910f613f-514f-4534-87dd-9b4699d59d31.png" width="800"/> </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> 丢失，立即重传 M<sub>3</sub>。

在这种情况下，只是丢失个别报文段，而不是网络拥塞。因此执行快恢复，令 ssthresh = cwnd / 2 ，cwnd = ssthresh，注意到此时直接进入拥塞避免。

慢开始和快恢复的快慢指的是 cwnd 的设定值，而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1，而快恢复 cwnd 设定为 ssthresh。

<div align="center"> <img src="pics/f61b5419-c94a-4df1-8d4d-aed9ae8cc6d5.png" width="600"/> </div><br>

### 15.0.2. TCP三次握手

*  为什么要三次握手，不能像http或者UDP一样直接传输

       主要是为了防止已失效的连接请求报文段突然又传到了B,因而报文错乱问题

  假定A发出的第一个连接请求报文段并没有丢失，而是在某些网络结点长时间滞留了，一直延迟到连接释放    以后的某个时间才到达B，本来这是一个早已失效的报文段。但B收到此失效的连接请求报文段后，就误认为   是A又发出一次新的连接请求，于是就向A发出确认报文段，同意建立连接。假定不采用三次握手，那么只要B发出确认，新的连接就建立了，这样一直等待A发来数据，B的许多资源就这样白白浪费了。

### 15.0.3. TCP四次挥手

**大厂面试TCP网络调度原理和TCP四次挥手设计**

* 面试题：你知道TCP四次挥手是什么吗？为什么要进行四次挥手

  * 确保数据能够完整传输

  * * 当被动方收到主动方的FIN报文通知时，它仅仅表示主动方没有数据再发送给被动方了。

    * 但未必被动方所有的数据都完整的发送给了主动方，所以被动方不会马上关闭SOCKET,它可能还需要发送一些数据给主动方后，再发送FIN报文给主动方，告诉主动方同意关闭连接
    * 所以这里的ACK报文和FIN报文多数情况下都是分开发送的。

  * 模拟流程

        A:“喂，我不说了 (FIN)。”A->FIN_WAIT1

        B:“我知道了(ACK)。等下，上一句还没说完。Balabala…..（传输数据）”B->CLOSE_WAIT | A->FIN_WAIT2

        B:”好了，说完了，我也不说了（FIN）。”B->LAST_ACK

        A:”我知道了（ACK）。”A->TIME_WAIT | B->CLOSED

        A等待2MSL,保证B收到了消息,否则重说一次”我知道了”,A->CLOSED

* 图解分析TCP四次挥手协议

  * TCP前面10种状态切换

* TCP第11种状态CLOSING 状态概念

  * 这种状态在实际情况中应该很少见，属于一种比较罕见的例外状态。正常情况下，当一方发送FIN报文后，按理来说是应该先收到（或同时收到）对方的ACK报文，再收到对方的FIN报文。但是CLOSING 状态表示一方发送FIN报文后，并没有收到对方的ACK报文，反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢？那就是当双方几乎在同时close()一个SOCKET的话，就出现了双方同时发送FIN报文的情况，这是就会出现CLOSING 状态，表示双方都正在关闭SOCKET连接。

* netstat -anp|grep 8080

# TCP粘包，UDP不粘包

基于TCP的socket编程

1.发送端为了将多个发往接收端的包，更有效的发到对方，使用了优化方法（Nagle算法），将多次间隔较小、数据量小的数据包，合并成一个大的数据包发送(把发送端的缓冲区填满一次性发送)。

2接收端底层会把tcp段整理排序交给缓冲区，这样接收端应用程序从缓冲区取数据就只能得到整体数据而不知道怎么拆分

这两个原因是tcp是面向流的协议

比如发送端发送了一个由2个100字节组成的200字节的数据包到接受端的缓冲区，接受端从缓冲去一次取80字节的数据，那么第一次取的就是一个不完整的数据包，第二次取就会带上第一个数据包的尾部和下一个数据包的头

注：一次取多少数据通过socket.recv(80)定义

## 为什么udp不会粘包

1.TCP协议是面向流的协议，UDP是面向消息的协议

UDP段都是一条消息，应用程序必须以消息为单位提取数据，不能一次提取任意字节的数据

2.UDP具有保护消息边界，在每个UDP包中就有了消息头（消息来源地址，端口等信息），这样对于接收端来说就容易进行区分处理了。传输协议把数据当作一条独立的消息在网上传输，接收端只能接收独立的消息。接收端一次只能接收发送端发出的一个数据包,如果一次接受数据的大小小于发送端一次发送的数据大小，就会丢失一部分数据，即使丢失，接受端也不会分两次去接收

# 127.0.0.1和0.0.0.0地址的区别

## 0.0.0.0
IPV4中，0.0.0.0地址被用于表示一个无效的，未知的或者不可用的目标。

**在服务器中**，0.0.0.0指的是本机上的所有IPV4地址，如果一个主机有两个IP地址，192.168.1.1 和 10.1.2.1，并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。

**在路由中**，0.0.0.0表示的是默认路由，即当路由表中没有找到完全匹配的路由的时候所对应的路由。

### 用途总结

当一台主机还没有被分配一个IP地址的时候，用于表示主机本身。（DHCP分配IP地址的时候）
用作默认路由，表示”任意IPV4主机”。
用来表示目标机器不可用。
用作服务端，表示本机上的任意IPV4地址。

## 127.0.0.1
127.0.0.1属于{127,}集合中的一个，而所有网络号为127的地址都被称之为回环地址，所以回环地址！=127.0.0.1,它们是包含关系，即回环地址包含127.0.0.1。
回环地址：所有发往该类地址的数据包都应该被loop back。

### 用途

回环测试,通过使用ping 127.0.0.1 测试某台机器上的网络设备，操作系统或者TCP/IP实现是否工作正常。

DDos攻击防御：网站收到DDos攻击之后，将域名A记录到127.0.0.1，即让攻击者自己攻击自己。

大部分Web容器测试的时候绑定的本机地址。
## 总结
172.16.58.3 是一个环回地址。并不表示“本机”。0.0.0.0才是真正表示“本网络中的本机”。

在实际应用中，一般我们在服务端绑定端口的时候可以选择绑定到0.0.0.0，这样我的服务访问方就可以通过我的多个ip地址访问我的服务。
比如我有一台服务器，一个外网地址A,一个内网地址B，如果我绑定的端口指定了0.0.0.0，那么通过内网地址或外网地址都可以访问我的应用。但是如果我只绑定了内网地址，那么通过外网地址就不能访问。 所以如果绑定0.0.0.0,也有一定安全隐患，对于只需要内网访问的服务，可以只绑定内网地址。

# Socket连接与HTTP,TCP

## TCP连接

要想明白Socket连接，先要明白TCP连接。手机能够使用联网功能是因为手机底层实现了TCP/IP协议，可以使手机终端通过无线网络建立TCP连接。TCP协议可以对上层网络提供接口，使上层网络数据的传输建立在“无差别”的网络之上。

建立起一个TCP连接需要经过“三次握手”：

第一次握手：客户端发送syn包(syn=j)到服务器，并进入SYN_SEND状态，等待服务器确认；

第二次握手：服务器收到syn包，必须确认客户的SYN（ack=j+1），同时自己也发送一个SYN包（syn=k），即SYN+ACK包，此时服务器进入SYN_RECV状态；

第三次握手：客户端收到服务器的SYN＋ACK包，向服务器发送确认包ACK(ack=k+1)，此包发送完毕，客户端和服务器进入ESTABLISHED状态，完成三次握手。

握手过程中传送的包里不包含数据，三次握手完毕后，客户端与服务器才正式开始传送数据。理想状态下，TCP连接一旦建立，在通信双方中的任何一方主动关闭连接之前，TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求，断开过程需要经过“四次握手”（过程就不细写了，就是服务器和客户端交互，最终确定断开）

## HTTP连接

HTTP协议即超文本传送协议(Hypertext Transfer Protocol )，是Web联网的基础，也是手机联网常用的协议之一，HTTP协议是建立在TCP协议之上的一种应用。

**HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应，在请求结束后，会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。**

1）在HTTP 1.0中，客户端的每次请求都要求建立一次单独的连接，在处理完本次请求后，就自动释放连接。

2）在HTTP 1.1中则可以在一次连接中处理多个请求，并且多个请求可以重叠进行，不需要等待一个请求结束后再发送下一个请求。

由于HTTP在每次请求结束后都会主动释放连接，因此HTTP连接是一种“短连接”，要保持客户端程序的在线状态，需要不断地向服务器发起连接请求。通常的做法是即时不需要获得任何数据，客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求，服务器在收到该请求后对客户端进行回复，表明知道客户端“在线”。若服务器长时间无法收到客户端的请求，则认为客户端“下线”，若客户端长时间无法收到服务器的回复，则认为网络已经断开。

## SOCKET原理

### 3.1套接字（socket）概念

套接字（socket）是通信的基石，是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示，包含进行网络通信必须的五种信息：**连接使用的协议，本地主机的IP地址，本地进程的协议端口，远地主机的IP地址，远地进程的协议端口。**

应用层通过传输层进行数据通信时，TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接，许多计算机操作系统为应用程序与TCP／IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口，区分来自不同应用程序进程或网络连接的通信，实现数据传输的并发服务。

### 3.2 建立socket连接

建立Socket连接至少需要一对套接字，其中一个运行于客户端，称为ClientSocket ，另一个运行于服务器端，称为ServerSocket 。

套接字之间的连接过程分为三个步骤：**服务器监听，客户端请求，连接确认。**

**服务器监听：**服务器端套接字并不定位具体的客户端套接字，而是处于等待连接的状态，实时监控网络状态，等待客户端的连接请求。

**客户端请求：**指客户端的套接字提出连接请求，要连接的目标是服务器端的套接字。为此，客户端的套接字必须首先描述它要连接的服务器的套接字，指出服务器端套接字的地址和端口号，然后就向服务器端套接字提出连接请求。

**连接确认：**当服务器端套接字监听到或者说接收到客户端套接字的连接请求时，就响应客户端套接字的请求，建立一个新的线程，把服务器端套接字的描述发给客户端，一旦客户端确认了此描述，双方就正式建立连接。而服务器端套接字继续处于监听状态，继续接收其他客户端套接字的连接请求。

## 4、SOCKET连接与TCP连接

创建Socket连接时，可以指定使用的传输层协议，Socket可以支持不同的传输层协议（TCP或UDP），当使用TCP协议进行连接时，该Socket连接就是一个TCP连接。

## 5、Socket连接与HTTP连接

由于通常情况下Socket连接就是TCP连接，因此Socket连接一旦建立，通信双方即可开始相互发送数据内容，直到双方连接断开。但在实际网络应用中，客户端到服务器之间的通信往往需要穿越多个中间节点，例如路由器、网关、防火墙等，大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连，因此需要通过轮询告诉网络，该连接处于活跃状态。

而HTTP连接使用的是“请求—响应”的方式，不仅在请求时需要先建立连接，而且需要客户端向服务器发出请求后，服务器端才能回复数据。

很多情况下，需要服务器端主动向客户端推送数据，保持客户端与服务器数据的实时与同步。**此时若双方建立的是Socket连接，服务器就可以直接将数据传送给客户端；若双方建立的是HTTP连接，则服务器需要等到客户端发送一次请求后才能将数据传回给客户端，因此，客户端定时向服务器端发送连接请求，不仅可以保持在线，同时也是在“询问”服务器是否有新的数据，如果有就将数据传给客户端。**

## **区别简述：**

1）http是一种协议，socket是一种编程接口，主要包括TCP协议和UDP协议;

2)http和TCP/UDP是两个不同层上的的协议。http是应用层的协议，TCP/UDP是传输层的协议，http是在TCP/UDP之上的协议，http协议使用了TCP/UDP,http更加高级一点但是没有很好的灵活性。也就是http使用起来比TCP/UDP要简单，只需要遵循规范就可以进行网络通信了。

<file_sep>/notes/Scala.md

## Scala的“=>”符号简介

`Scala`中的`=>`符号可以看做是创建函数实例的语法糖。例如：`A => T`，`A,B => T`表示一个函数的输入参数类型是“`A`”，“`A,B`”，返回值类型是`T`。请看下面这个实例：

```
scala> val f: Int => String = myInt => "The value of myInt is: " + myInt.toString()
f: Int => String = <function1>

scala> println(f(3))
The value of myInt is: 3
```

上面例子定义函数`f`：输入参数是整数类型，返回值是字符串。

另外，`() => T`表示函数输入参数为空，而`A => Unit`则表示函数没有返回值。

# 关键字Lazy

Scala中使用关键字lazy来定义惰性变量，实现延迟加载(懒加载)。
惰性变量只能是不可变变量，并且只有在调用惰性变量时，才会去实例化这个变量。

# 作用域保护

Scala中，访问修饰符可以通过使用限定词强调。格式为:

```
private[x]

或

protected[x]
```

这里的x指代某个所属的包、类或单例对象。如果写成private[x],读作"这个成员除了对[…]中的类或[…]中的包中的类及它们的伴生对像可见外，对其它所有类都是private。

这种技巧在横跨了若干包的大型项目中非常有用，它允许你定义一些在你项目的若干子包中可见但对于项目外部的客户却始终不可见的东西。

```
package bobsrockets{
    package navigation{
        private[bobsrockets] class Navigator{
         protected[navigation] def useStarChart(){}
         class LegOfJourney{
             private[Navigator] val distance = 100
             }
            private[this] var speed = 200
            }
        }
        package launch{
        import navigation._
        object Vehicle{
        private[launch] val guide = new Navigator
        }
    }
}
```

上述例子中，类Navigator被标记为private[bobsrockets]就是说这个类对包含在bobsrockets包里的所有的类和对象可见。

比如说，从Vehicle对象里对Navigator的访问是被允许的，因为对象Vehicle包含在包launch中，而launch包在bobsrockets中，相反，所有在包bobsrockets之外的代码都不能访问类Navigator。

# Case Class

Case Class一般被翻译成样例类，它是一种特殊的类，能够被优化以用于模式匹配，下面的代码定义了一个样例类：

```scala
//抽象类Person
abstract class Person

//case class Student
case class Student(name:String,age:Int,studentNo:Int) extends Person
//case class Teacher
case class Teacher(name:String,age:Int,teacherNo:Int) extends Person
//case class Nobody
case class Nobody(name:String) extends Person

object CaseClassDemo{
  def main(args: Array[String]): Unit = {
    //case class 会自动生成apply方法，从而省去new操作
    val p:Person=Student("john",18,1024)
    //match case 匹配语法
    p  match {
      case Student(name,age,studentNo)=>println(name+":"+age+":"+studentNo)
      case Teacher(name,age,teacherNo)=>println(name+":"+age+":"+teacherNo)
      case Nobody(name)=>println(name)
    }
  }
}

```

当一个类被声名为case class的时候，scala会帮助我们做下面几件事情：
1 构造器中的参数如果不被声明为var的话，它默认的话是val类型的，但一般不推荐将构造器中的参数声明为var
2 自动创建伴生对象，同时在里面给我们实现子apply方法，使得我们在使用的时候可以不直接显示地new对象
3 伴生对象中同样会帮我们实现unapply方法，从而可以将case class应用于模式匹配，关于unapply方法我们在后面的“提取器”那一节会重点讲解
4 实现自己的toString、hashCode、copy、equals方法
除此之此，case class与其它普通的scala类没有区别

## case class在实用应用中的其它用途

某个类一旦被定义为case class，则编译器会自动生成该类的伴生对象，伴生对象中包括了apply方法及unapply方法，apply方法使得我们可以不需要new关键字就可以创建对象，而unapply方法，则使得可以方便地应用在模式匹配当中，另外编译器还自动地帮我们实现对应的toString、equals、copy等方法

# Scala的Option

Scala `Option[T]`是由给定类型的零或一个元素的一种容器。`Option[T]`可以是 `Some [T]`或`None`对象，它代表缺少的值。 例如，如果已找到与给定键对应的值，则Scala的Map的`get`方法会生成`Some(value)`，如果在`Map`中未定义给定的键，则将返回`None`。

`Option`类型在Scala程序中经常使用，可以将其与Java中可用的`null`值进行比较，表示`null`值。 例如，`java.util.HashMap`的`get`方法返回存储在`HashMap`中的值，如果没有找到值，则返回`null`。

假设我们有一种基于主键从数据库中检索记录的方法。

```scala
def findPerson(key: Int): Option[Person]
```

如果找到记录，该方法将返回`Some [Person]`，如果没有找到该记录，则返回`None`。下面来看看看以下程序代码 -

**示例**

```scala
object Demo {
   def main(args: Array[String]) {
      val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")

      println("capitals.get( \"France\" ) : " +  capitals.get( "France" ))
      println("capitals.get( \"India\" ) : " +  capitals.get( "India" ))
   }
}

```

将上述程序保存在源文件：*Demo.scala*中，使用以下命令编译和执行此程序。

```shell
D:\>scalac Demo.scala
D:\>scala Demo
capitals.get( "France" ) : Some(Paris)
capitals.get( "India" ) : None

```

将可选值分开的最常见方法是通过模式匹配。例如尝试以下程序 -

```scala
object Demo {
   def main(args: Array[String]) {
      val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")

      println("show(capitals.get( \"Japan\")) : " + show(capitals.get( "Japan")) )
      println("show(capitals.get( \"India\")) : " + show(capitals.get( "India")) )
   }

   def show(x: Option[String]) = x match {
      case Some(s) => s
      case None => "?"
   }
}

Scala
```

将上述程序保存在源文件：*Demo.scala*中，使用以下命令编译和执行此程序。

```shell
D:\>scalac Demo.scala
D:\>scala Demo
show(capitals.get( "Japan")) : Tokyo
show(capitals.get( "India")) : ?

Shell
```

## 使用getOrElse()方法

以下是示例程序，显示如何在没有值的情况下使用`getOrElse()`方法来访问值或默认值。

```scala
object Demo {
   def main(args: Array[String]) {
      val a:Option[Int] = Some(5)
      val b:Option[Int] = None

      println("a.getOrElse(0): " + a.getOrElse(0) )
      println("b.getOrElse(10): " + b.getOrElse(10) )
   }
}

Scala
```

将上述程序保存在源文件：*Demo.scala*中，使用以下命令编译和执行此程序。

```shell
D:\>scalac Demo.scala
D:\>scala Demo
a.getOrElse(0): 5
b.getOrElse(10): 10

Shell
```

## 使用isEmpty()方法

以下是显示如何使用`isEmpty()`方法检查该选项是否为`None`的示例程序。

```scala
object Demo {
   def main(args: Array[String]) {
      val a:Option[Int] = Some(5)
      val b:Option[Int] = None

      println("a.isEmpty: " + a.isEmpty )
      println("b.isEmpty: " + b.isEmpty )
   }
}

Scala
```

将上述程序保存在源文件：*Demo.scala*中，使用以下命令编译和执行此程序。

```shell
D:\>scalac Demo.scala
D:\>scala Demo
a.isEmpty: false
b.isEmpty: true
```

# 对象与类

## object

在scala中没有静态方法和静态字段，所以在scala中可以用object来实现这些功能，直接用对象名调用的方法都是采用这种实现方式，例如Array.toString。对象的构造器在第一次使用的时候会被调用，如果一个对象从未被使用，那么他的构造器也不会被执行；对象本质上拥有类（scala中）的所有特性，除此之外，object还可以一扩展类以及一个或者多个特质：例如，

```
abstract class ClassName（val parameter）{}

object Test extends ClassName(val parameter){}

trait TraitA{}
trait TraitB{}
trait TraitC{}
object Test1 extends TraitA with TraitB with TraitC{}
注意：object不能提供构造器参数，也就是说object必须是无参的
```

我们通常会定义和使用object的apply方法，有如下情形，apply方法就会被调用，Object（参数1 ，参数2 ，……）这样是隐含的调用apply方法，当然也可以显示的调用，如下Array（"Mary","tong"）,当然也可以这样来写Array.apply（"Mary","tong"）,一般我们提倡隐式的写法。

所有的main方法都必须在object中被调用，来提供程序的主入口，十分简单，不举例说明，处理main方法以外，scala中还提供了扩展App特质，然后将程序代码放入都早起方法体内，如

```
object Hello extends App{

   println（"Hello World ！！！"）

}
```

这样的代码可以直接执行，输出

如果需要命令行参数，可以直接调用args属性来得到，例如

```
def main(args: Array[String]) {
  if (args.length > 0 )
    println("Hello " + args(0))
  else
    println("Hello World")
}
```

## class

在scala中，类名可以和对象名为同一个名字，该对象称为该类的伴生对象，类和伴生对象可以相互访问他们的私有属性，但是
他们必须在同一个源文件内。类只会被编译，不能直接被执行，类的申明和主构造器在一起被申明，在一个类中，主构造器只有一个，
所有必须在内部申明主构造器或者是其他申明主构造器的辅构造器，主构造器会执行类定义中的所有语句。scala对每个字段都会提供
getter和setter方法，同时也可以显示的申明，但是针对val类型，只提供getter方法，默认情况下，字段为公有类型，可以在setter
方法中增加限制条件来限定变量的变化范围，在scala中方法可以访问改类所有对象的私有字段

## var与val

在 Scala 中，使用关键词 **"var"** 声明变量，使用关键词 **"val"** 声明常量。

声明变量实例如下：

```
var myVar : String = "Foo"
var myVar : String = "Too"
```

以上定义了变量 myVar，我们可以修改它。

声明常量实例如下：

```
val myVal : String = "Foo"
```

以上定义了常量 myVal，它是不能修改的。如果程序尝试修改常量 myVal 的值，程序将会在编译时报错。

## 变量类型引用

在 Scala 中声明变量和常量不一定要指明数据类型，在没有指明数据类型的情况下，其数据类型是通过变量或常量的初始值推断出来的。

所以，如果在没有指明数据类型的情况下声明变量或常量必须要给出其初始值，否则将会报错。

```
var myVar = 10;
val myVal = "Hello, Scala!";
```

# Scala Trait(特征)

Scala Trait(特征) 相当于 Java 的接口，实际上它比接口还功能强大。

与接口不同的是，它还可以定义属性和方法的实现。

一般情况下Scala的类只能够继承单一父类，但是如果是 Trait(特征) 的话就可以继承多个，从结果来看就是实现了多重继承。

Trait(特征) 定义的方式与类类似，但它使用的关键字是 **trait**，如下所示：

```
trait Equal {
  def isEqual(x: Any): Boolean
  def isNotEqual(x: Any): Boolean = !isEqual(x)
}
```

以上Trait(特征)由两个方法组成：**isEqual** 和 **isNotEqual**。isEqual 方法没有定义方法的实现，isNotEqual定义了方法的实现。子类继承特征可以实现未被实现的方法。所以其实 Scala Trait(特征)更像 Java 的抽象类。

以下演示了特征的完整实例：

```
/* 文件名：Test.scala
 * author:菜鸟教程
 * url:www.runoob.com
 */
trait Equal {
  def isEqual(x: Any): Boolean
  def isNotEqual(x: Any): Boolean = !isEqual(x)
}

class Point(xc: Int, yc: Int) extends Equal {
  var x: Int = xc
  var y: Int = yc
  def isEqual(obj: Any) =
    obj.isInstanceOf[Point] &&
    obj.asInstanceOf[Point].x == x
}

object Test {
   def main(args: Array[String]) {
      val p1 = new Point(2, 3)
      val p2 = new Point(2, 4)
      val p3 = new Point(3, 3)

      println(p1.isNotEqual(p2))
      println(p1.isNotEqual(p3))
      println(p1.isNotEqual(2))
   }
}
```

执行以上代码，输出结果为：

```
$ scalac Test.scala
$ scala Test
false
true
true
```

# with关键字

```
class A extends B with C with D with E 1
```

解读方式：

```
class A extends (B with C with D with E)1
```

*(B with C with D with E)*首先是一个整体，再由 A 去继承。
注：对于Java中的菱形继承问题；Scala的解决策略是：
\1. 继承*class*混入*trait*：(*extends SuperClass with subClass|subTrait with …* ) 必须是一个继承链(左->右)
\2. 继承无关的多*trait*：(*class extends T1 with T2 with …*), T1和T2中相同字段或方法时，会编译错误-inherits conflicting members…(继承成员冲突)。如：

```
trait T1 { val a = 1 }
trait T2 { val b = 2 }
class SubClass extends T1 with T2123
```

编译会报错：Error:(7, 7) class SubClass inherits conflicting members:

------

**复合类型**：

```
T1 with T2 with T3 ...1
```

这种形式的类型-**复合类型**(compound type)，或**交集类型**(intersection type)。

- 在方法声明***参数类型\*** 时使用复合类型：

```
def func (x: T1 with T2) = { println("hello") }

func(new T1 with T2)123
```

- ***type声明\*** 复合类型：

```
type T = X1 with X2

def func (x: T) = { println("hello") }123
```

- 复合类型中使用***结构类型\***：

```
def func (x: X1 with X2 { def hello(): Unit }) = { println("hello") }
```

<file_sep>/notes/剑指offer.md

<!-- TOC -->

- [如何写算法](#如何写算法)
    - [职业选手的培养方法](#职业选手的培养方法)
    - [面试五步](#面试五步)
    - [五遍刷题](#五遍刷题)
- [常见的数据结构的用法](#常见的数据结构的用法)
- [3. 数组中重复的数字](#3-数组中重复的数字)
- [4. 二维数组中的查找](#4-二维数组中的查找)
- [5. 替换空格](#5-替换空格)
- [6. 从尾到头打印链表](#6-从尾到头打印链表)
- [7. 重建二叉树](#7-重建二叉树)
- [@@8. 二叉树的下一个结点](#8-二叉树的下一个结点)
- [@@9. 用两个栈实现队列](#9-用两个栈实现队列)
- [10.1 斐波那契数列](#101-斐波那契数列)
- [10.2 矩形覆盖](#102-矩形覆盖)
- [10.3 跳台阶](#103-跳台阶)
- [10.4 变态跳台阶](#104-变态跳台阶)
- [11. 旋转数组的最小数字](#11-旋转数组的最小数字)
- [12. 矩阵中的路径](#12-矩阵中的路径)
- [13. 机器人的运动范围](#13-机器人的运动范围)
- [@14. 剪绳子](#14-剪绳子)
- [15. 二进制中 1 的个数](#15-二进制中-1-的个数)
- [@16. 数值的整数次方](#16-数值的整数次方)
- [@17. 打印从 1 到最大的 n 位数](#17-打印从-1-到最大的-n-位数)
- [18.1 在 O(1) 时间内删除链表节点](#181-在-o1-时间内删除链表节点)
- [18.2 删除链表中重复的结点](#182-删除链表中重复的结点)
- [@@@19. 正则表达式匹配](#19-正则表达式匹配)
- [@20. 表示数值的字符串](#20-表示数值的字符串)
- [@21. 调整数组顺序使奇数位于偶数前面](#21-调整数组顺序使奇数位于偶数前面)
- [22. 链表中倒数第 K 个结点](#22-链表中倒数第-k-个结点)
- [23. 链表中环的入口结点](#23-链表中环的入口结点)
- [24. 反转链表](#24-反转链表)
- [25. 合并两个排序的链表](#25-合并两个排序的链表)
- [26. 树的子结构](#26-树的子结构)
- [27. 二叉树的镜像](#27-二叉树的镜像)
- [28 对称的二叉树](#28-对称的二叉树)
- [29. 顺时针打印矩阵](#29-顺时针打印矩阵)
- [30. 包含 min 函数的栈](#30-包含-min-函数的栈)
- [@31. 栈的压入、弹出序列](#31-栈的压入弹出序列)
- [32.1 从上往下打印二叉树](#321-从上往下打印二叉树)
- [32.2 把二叉树打印成多行](#322-把二叉树打印成多行)
- [32.3 按之字形顺序打印二叉树](#323-按之字形顺序打印二叉树)
- [@@@@33. 二叉搜索树的后序遍历序列](#33-二叉搜索树的后序遍历序列)
- [34. 二叉树中和为某一值的路径](#34-二叉树中和为某一值的路径)
- [@@35. 复杂链表的复制](#35-复杂链表的复制)
- [@@36. 二叉搜索树与双向链表](#36-二叉搜索树与双向链表)
- [37. 序列化二叉树](#37-序列化二叉树)
- [38. 字符串的排列](#38-字符串的排列)
- [39. 数组中出现次数超过一半的数字](#39-数组中出现次数超过一半的数字)
- [@@@40. 最小的 K 个数](#40-最小的-k-个数)
        - [快速选择](#快速选择)
        - [大小为 K 的最小堆](#大小为-k-的最小堆)
- [快速排序](#快速排序)
- [41.1 数据流中的中位数](#411-数据流中的中位数)
- [41.2 字符流中第一个不重复的字符](#412-字符流中第一个不重复的字符)
- [42. 连续子数组的最大和](#42-连续子数组的最大和)
- [@@42.2二维矩阵中的最大子矩阵](#422二维矩阵中的最大子矩阵)
- [@@@@43. 从 1 到 n 整数中 1 出现的次数](#43-从-1-到-n-整数中-1-出现的次数)
- [@44. 数字序列中的某一位数字](#44-数字序列中的某一位数字)
- [@45. 把数组排成最小的数](#45-把数组排成最小的数)
- [@46. 把数字翻译成字符串](#46-把数字翻译成字符串)
- [47. 礼物的最大价值](#47-礼物的最大价值)
- [48. 最长不含重复字符的子字符串](#48-最长不含重复字符的子字符串)
- [@@49. 丑数](#49-丑数)
- [50. 第一个只出现一次的字符位置](#50-第一个只出现一次的字符位置)
- [@@51. 数组中的逆序对](#51-数组中的逆序对)
- [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点)
- [53. 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数)
- [54. 二叉查找树的第 K 个结点](#54-二叉查找树的第-k-个结点)
- [55.1 二叉树的深度](#551-二叉树的深度)
- [55.2 平衡二叉树](#552-平衡二叉树)
- [@56. 数组中只出现一次的数字](#56-数组中只出现一次的数字)
- [57.1 和为 S 的两个数字](#571-和为-s-的两个数字)
- [57.2 和为 S 的连续正数序列](#572-和为-s-的连续正数序列)
- [@58.1 翻转单词顺序列](#581-翻转单词顺序列)
- [@58.2 左旋转字符串](#582-左旋转字符串)
- [59. 滑动窗口的最大值](#59-滑动窗口的最大值)
- [@ 60. n 个骰子的点数](#-60-n-个骰子的点数)
- [@61. 扑克牌顺子](#61-扑克牌顺子)
- [@62. 圆圈中最后剩下的数](#62-圆圈中最后剩下的数)
- [63. 股票的最大利润](#63-股票的最大利润)
- [@@64. 求 1+2+3+...+n](#64-求-123n)
- [@@65. 不用加减乘除做加法](#65-不用加减乘除做加法)
- [66. 构建乘积数组](#66-构建乘积数组)
- [@@@67. 把字符串转换成整数](#67-把字符串转换成整数)
- [@@68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先)
        - [二叉查找树](#二叉查找树)
        - [普通二叉树](#普通二叉树)
- [70.对称的二叉树](#70对称的二叉树)
- [@@71.数据流中的中位数](#71数据流中的中位数)

<!-- /TOC -->

# 如何写算法

## 职业选手的培养方法

1.知识体系(链表数组栈队列,树图,bitmap,动态规划,排序...)

2.刻意练习(那里不熟练哪里,每道题五遍以上)

3.注意反馈(主动反馈:算法选手直播,代码范例   被动反馈:codereview)

## 面试五步

1.搞清楚问题是什么,跟面试官复述一遍,确定一些边界(空,格式错误,百万以上的大数)

2.可能的解法有多种,都跟面试官说明一下,分析他们的时间空间复杂度.

3.确定整体思路

4.着手写代码

5.自己写testCase来做测试

## 五遍刷题

第一遍,没有思路就直接看解法

<img src="./pictures/jianzhi-offer/Snipaste_2019-12-15_12-55-12.png" style="zoom:50%;" />

<img src="./pictures/jianzhi-offer/Snipaste_2019-12-15_12-58-34.png" style="zoom:50%;" />

第三遍一天后再刷

第四遍一周后再刷

第五遍考前适应性训练

# 常见的数据结构的用法

```
map:
HashMap<Integer,Integer> hm = new HashMap<Integer,Integer>();

遍历:
Set<Map.Entry<Integer,Integer>> entrySet = hm.entrySet();
Iterator<Map.Entry<Integer,Integer>> it = entrySet.iterator();
while(it.hasNext()){
	Map.Entry<Integer,Integer> entry =it.next();
	entry.getKey();
	entry.getValue();
}
遍历2:
for(Map.Entry<Integer,Integer> temp:hm.entrySet()){
	temp.getKey();
	temp.getValue();
}

队列和链表

链表
LinkedList<Integer> list = new LinkedList<Integer>();

list.add(1111);
list.add(0,111);//头插
list.peek();//返回头结点,不删除
list.poll(); //返回头结点,删除
list.get(0);//获取0处的节点

遍历1:
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
	int i = (int)it.next();
}
遍历2:
for(Integer temp:list){
	int a  =temp;
}

队列:
Queue<Integer> queue = new LinkedList<Integer>();
q.poll();
q.remove();

q.add(1);
q.offer(1);

q.peek();

q.isEmpty();

栈

Stack<Integer> stack1 = new Stack<Integer>();
stack1.pop();
stack1.push(1);
stack1.peek();
stack1.isEmpty();

ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(integers));

ArrayList

        ArrayList<Integer> list = new ArrayList<Integer>();

        list.add(0);
        list.add(0, 1);

        list.get(0);

        list.remove(1);
        list.remove(new Integer(1));

list转array
        Object[] objects = list.toArray();
        Integer[] integers = list.toArray(new Integer[list.size()]);

array转list
方法1.  ArrayList<Integer> integers1 = new ArrayList<>(Arrays.asList(1,2,3,4,5));
	但是缺点如下:
	此处asList里面放的是 不定数量的数字,而不是array数组   public static <T> List<T> asList(T... a)
	而且无法对int[] array这种数组进行转换,只能转换Integer

方法2.   	String[] strArray = new String[2];
        ArrayList< String> arrayList = new ArrayList<String>(strArray.length);
        Collections.addAll(arrayList, strArray);
        arrayList.add("1");
        System.out.println(arrayList);
     注意:无法对int[] array这种数组进行转换,只能转换Integer

Collections.sort();
Collections.binarySearch();
Collections.addAll();

Arrays.sort();
Arrays.asList();
Arrays.toString();
Arrays.binarySearch();

String 转 char[]   str.toCharArray()
str.substring();
str.charAt();
ch1.equals(ch2);

双端队列
ArrayDeque<Integer> deque = New ArrayDeque<Integer>();
deque.getFirst();deque.getLast();
deque.removeFirst();deque.removeLast();
deque.addFirst();deque.addLast();

数组是引用传递,函数内部修改后,函数外部也会收到变化.

```

# 3. 数组中重复的数字

[NowCoder](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的，但不知道有几个数字是重复的，也不知道每个数字重复几次。请找出数组中任意一个重复的数字。

```html
Input:
{2, 3, 1, 0, 2, 5}

Output:
2
```

**解题思路**

要求时间复杂度 O(N)，空间复杂度 O(1)。因此不能使用排序的方法，也不能使用额外的标记数组。

对于这种数组元素在 [0, n-1] 范围内的问题，可以将值为 i 的元素调整到第 i 个位置上进行求解。

以 (2, 3, 1, 0, 2, 5) 为例，遍历到位置 4 时，该位置上的数为 2，但是第 2 个位置上已经有一个 2 的值了，因此可以知道 2 重复：

```java
  public class Solution {
            // Parameters:
            //    numbers:     an array of integers
            //    length:      the length of array numbers
            //    duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
            //                  Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
            //    这里要特别注意~返回任意重复的一个，赋值duplication[0]
            // Return value:       true if the input is valid, and there are some duplications in the array number
            //                     otherwise false
            public boolean duplicate(int numbers[], int length, int[] duplication) {
                if (numbers == null || length < 0) {
                    return false;
                }
                for (int i = 0; i < length; i++) {
                    while (i != numbers[i]) {
                        if (numbers[i] == numbers[numbers[i]]) {
                            duplication[0] = numbers[i];
                            return true;
                        }
                        swap(i, numbers[i], numbers);
                    }

                }
                return false;
            }

            private void swap(int index1, int index2, int[] numbers) {
                int c = numbers[index1];
                numbers[index1] = numbers[index2];
                numbers[index2] = c;
            }
        }
```

改进

1.数组判空

2.for循环改成while更符合逻辑

```java
    public boolean duplicate(int numbers[], int length, int[] duplication) {
         if (numbers == null || length < 0) {
                    return false;
         }
        //遍历数组
        //数字放到对应下标处
        //做交换
        //如果该出已有与下标相同,则返回
        //如果下标不同,则放入后,重复此过程
        int idx;
        int temp;
        for (int i = 0; i < length; ) {
            if (numbers[i] != i) {
                idx = numbers[i];
                if (numbers[idx] == idx) {
                    duplication[0] = idx;
                    return true;
                }
                temp = numbers[idx];
                numbers[idx] = numbers[i];
                numbers[i] = temp;
                //再循环,不后移
            } else {
                i++;
            }
        }
        return false;
    }
```

# 4. 二维数组中的查找

[NowCoder](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

给定一个二维数组，其每一行从左到右递增排序，从上到下也是递增排序。给定一个数，判断这个数是否在该二维数组中。

```html
Consider the following matrix:
[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

Given target = 5, return true.
Given target = 20, return false.
```

**解题思路**

要求时间复杂度 O(M + N)，空间复杂度 O(1)。其中 M 为行数，N 为 列数。
从左下角开始查找.

如果只是每一层使用快排,那么其实只利用了每一行有序的条件,没有使用每一列也有序,复杂度为O(nlgn)

```java
  public boolean Find(int target, int[][] array) {
        //rows和cols的命名方式
        int rows = array.length;
        int cols = array[0].length;
        //从左下角开始
        int i = rows - 1;
        int j = 0;
        //此处使用while比使用for要简洁
        while (i >= 0 && j < cols) {
            if (target == array[i][j])
                return true;
            else if (target > array[i][j])
                j++;
            else
                i--;
        }
        return  false;
    }
```

改进

1.row--后,col不用从零开始,因为`target>array[row][col]>array[row--][col]>array[row--][0到col-1]`

2.注意判断数组越界

```java

public class Solution {
    public boolean Find(int target, int[][] array) {
        //先画图

        //判空
        //从左下角开始
        //比上面大  行--
        //比左边大  列++
        //到头了没找到就退出
        int row = array.length - 1, col = 0;
        int rowSize = array.length;
        int colSize = array[0].length;
        while (row >= 0 && col < colSize) {
            while (row >= 0 && col < colSize && target < array[row][0]) {
                row--;
            }
            while (row >= 0 && col < colSize && target > array[row][col]) {
                col++;
            }
            if (row >= 0 && col < colSize && target == array[row][col]) {
                return true;
            }
            row--;
        }
        return false;
    }
}
```

# 5. 替换空格

[NowCoder](https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423?tpId=13&tqId=11155&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

将一个字符串中的空格替换成 "%20"。

```text
Input:
"A B"

Output:
"A%20B"
```

**解题思路**
时间O(n) 空间O(n)
新创建一个StringBuffer来存储结果
```java
public class Solution {
    public String replaceSpace(StringBuffer str) {
    	StringBuilder  sb  = new StringBuilder();
        for(int i =0;i<str.length();i++){
            char c = str.charAt(i);
            if(c==' '){
                sb.append("%20");
            }else{
                sb.append(c);
            }
        }
        return sb.toString();
    }
}

```

改进:

时间O(n) 空间O(1)

**提前遍历一遍字符串,可以确定里面空格的个数,复杂度依然保持O(n)**

```java
public class Solution {
    public String replaceSpace(StringBuffer str) {
        //遍历,寻找空格个数,设置长度是空格数+2
        //倒序遍历,空格处给str设置为%20
        int oldPointer = str.length() - 1;
        int newPointer;
        for (int i = 0; i <= oldPointer; i++) {
            if (str.charAt(i) == ' ') {
                str.append("  ");
            }
        }
        newPointer = str.length() - 1;
        for (; oldPointer >= 0; oldPointer--) {
            if (str.charAt(oldPointer) == ' ') {
                str.setCharAt(newPointer--, '0');
                str.setCharAt(newPointer--, '2');
                str.setCharAt(newPointer--, '%');
            } else {
                str.setCharAt(newPointer--, str.charAt(oldPointer));
            }
        }
        return str.toString();
    }
}
```

# 6. 从尾到头打印链表

[NowCoder](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

从尾到头反过来打印出每个结点的值。

时间复杂度一般是O(n),空间复杂度可以是O(1)或者O(n),另外还要注意是否可以改变输入的数据结构

**解题思路**

**使用递归**

要逆序打印链表 1->2->3（3,2,1)，可以先逆序打印链表 2->3(3,2)，最后再打印第一个节点 1。而链表 2->3 可以看成一个新的链表，要逆序打印该链表可以继续使用求解函数，也就是在求解函数中调用自己，这就是递归函数。

```java
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> ret = new ArrayList<>();
    if (listNode != null) {
        ret.addAll(printListFromTailToHead(listNode.next));
        ret.add(listNode.val);
    }
    return ret;
}

改进:
从头到尾的逆序 考虑用递归或者栈
2N的复杂度依然是O(N)
ArrayList的addAll函数
前面写了接收类型,后面可不写:
ArrayList<Integer> reverseList = new ArrayList<>();
在本层判断本层的listNode!=null;而不是判断下一层 if (listNode.next != null)

public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        //创建一个链表
        //如果还有后续节点,就调用后续,并把结果放到当前链表上
        //返回当前链表
        if (listNode == null) {
            return new ArrayList<Integer>();
        }
        ArrayList<Integer> reverseList = new ArrayList<>();

        if (listNode.next != null) {
            ArrayList<Integer> ret = printListFromTailToHead(listNode.next);
            reverseList.addAll(ret);
        }
        reverseList.add(listNode.val);
        return reverseList;
    }
}
```

**使用头插法**

使用头插法可以得到一个逆序的链表。

list.add(0,value);//实现头插

头结点和第一个节点的区别：

- 头结点是在头插法中使用的一个额外节点，这个节点不存储值；
- 第一个节点就是链表的第一个真正存储值的节点。

```java

/**
*    public class ListNode {
*        int val;
*        ListNode next = null;
*
*        ListNode(int val) {
*            this.val = val;
*        }
*    }
*
*/
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList arrayList = new ArrayList();
        while(listNode!=null){
            //头插法的可用函数
            arrayList.add(0,listNode.val);
            listNode = listNode.next;
        }
        return arrayList;
    }
}

改进:
arrayList有add(0,value)的函数,可以实现头插
```
或者
```java
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ListNode head = new ListNode(-1);
    //头插法构建逆序链表
    while (listNode != null) {
        ListNode memo = listNode.next;
        listNode.next = head.next;
        head.next = listNode;
        listNode = memo;
    }
    //逆序链表放入 ArrayList
    ArrayList<Integer> ret = new ArrayList<>();
    head = head.next;
    while (head != null) {
        ret.add(head.val);
        head = head.next;
    }
    return ret;
}

改进
    头插的链表放到list里面的时候,记得的第一个head节点给跳过	head = head.next;
```

使用栈

栈具有后进先出的特点，在遍历链表时将值按顺序放入栈中，最后出栈的顺序即为逆序。

```java
//注意导入stack包
import java.util.Stack;
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
         Stack<Integer> stack  = new Stack<>();
        ArrayList<Integer> arrayList = new ArrayList<>();
        while (listNode != null) {
            //stack的add方法来自它继承的List父类,效果与push相同
            stack.push(listNode.val);
            listNode = listNode.next;
        }
        while (!stack.isEmpty()) {
            arrayList.add((Integer) stack.pop());
        }
        return arrayList;
    }
}
```

# 7. 重建二叉树

[NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

根据二叉树的前序遍历和中序遍历的结果，重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

**解题思路**
数组的长度用length而不是size

本题treenode的构造器没有无参构造器,注意报错

HashMap<integer,integer> = new HashMap<integer,integer>();等号两遍泛型都填好,防止歧义;注意要导入包`import java.util.HashMap`

前序遍历的第一个值为根节点的值，使用这个值将中序遍历结果分成两部分，左部分为树的左子树中序遍历结果，右部分为树的右子树中序遍历的结果。
```java
/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
import java.util.HashMap;
public class Solution {
    HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if(pre==null||in==null){
            return null;
        }
        for(int i = 0;i<in.length;i++){
            map.put(in[i],i);
        }
        return reConstructBinaryTree(pre,0,pre.length-1,0);
    }
    public TreeNode reConstructBinaryTree(int [] pre,int preL,int preR,int inL){
        if(preL>preR){
            return null;
        }
        int rootVal = pre[preL];
        TreeNode tree = new TreeNode(rootVal);
        int inIndex = map.get(rootVal);
        int leftSize = inIndex-inL;
        tree.left = reConstructBinaryTree(pre,preL+1,preL+leftSize,inL);
        tree.right = reConstructBinaryTree(pre,preL+leftSize+1,preR,inIndex+1);
        return tree;
    }
}

改进点
    命名的规范清晰,为逻辑的编写 和 debug提供了极大的方便,继续保持
    要利用函数重载的技巧,缩减重复写的代码
    利用map记录下了每个点的位置,parentLoc的定位会减少计算量

    public class Solution {
    public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
        //如果前序数组长度为0,则返回
        //递归
        //前序确定根节点
        //中序拆分左右子树
        //将返回的结果放入左右子树

        //重复上述步骤
        int parentLoc = 0;
        int leftChildLength = 0;
        int rightChildLength = 0;
        int inL = 0;
        int inR = in.length - 1;
        int preL = 0;
        int preR = pre.length - 1;

        if (pre.length <= 0) {
            return null;
        }
        TreeNode parent = new TreeNode(pre[preL]);
        for (int i = inL; i <= inR; i++) {
            if (in[i] == parent.val) {
                parentLoc = i;
                break;
            }
        }
        leftChildLength = parentLoc - inL;
        rightChildLength = inR - parentLoc;

        parent.left = reConstructBinaryTree(pre, preL + 1, preL + leftChildLength, in, inL, inL + leftChildLength);
        parent.right = reConstructBinaryTree(pre, preL + leftChildLength + 1, preR, in, parentLoc + 1, inR);

        parent = reConstructBinaryTree(pre, preL, preR, in, inL, inR);
        return parent;

    }

    private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, int inL, int inR) {
        int parentLoc = 0;
        int leftChildLength = 0;
        int rightChildLength = 0;

        if (preR < preL || inR < inL) {
            return null;
        }
        TreeNode parent = new TreeNode(pre[preL]);
        for (int i = inL; i <= inR; i++) {
            if (in[i] == parent.val) {
                parentLoc = i;
                break;
            }
        }
        leftChildLength = parentLoc - inL;
        rightChildLength = inR - parentLoc;

        parent.left = reConstructBinaryTree(pre, preL + 1, preL + leftChildLength, in, inL, inL + leftChildLength);
        parent.right = reConstructBinaryTree(pre, preL + leftChildLength + 1, preR, in, parentLoc + 1, inR);
        return parent;
    }
}

```

# @@8. 二叉树的下一个结点

[NowCoder](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

@1.有右子树,就找最左边

@2.无右子树, 则pNode分为 1. pNode 是左子树,2.pNode是右子树

**题目描述**

给定一个二叉树和其中的一个结点，请找出中序遍历顺序的下一个结点并且返回。注意，树中的结点不仅包含左右子结点，同时包含指向父结点的指针。

**解题思路**

① 如果一个节点的右子树不为空，那么该节点的下一个节点是右子树的最左节点；

<div align="center"> <img src="pics/b0611f89-1e5f-4494-a795-3544bf65042a.gif" width="220px"/> </div><br>
② 否则，向上找第一个左链接指向的树包含该节点的祖先节点。

```java
public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        if (pNode == null) {
            return null;
        }
        if (pNode.right != null) {
            TreeLinkNode node = pNode.right;
            while (node.left != null) {
                node = node.left;
            }
            return node;
        } else {
            while (pNode.next != null) {
                TreeLinkNode parent = pNode.next;
                if (parent.left == pNode) {
                    return parent;
                }
                pNode = pNode.next;

            }
        }
        return null;
    }
}

class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}

改进:
思路很重要,如果没有右子树,就找该node作为父亲节点左子节点的父亲节点
```

# @9. 用两个栈实现队列

[NowCoder](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

用两个栈来实现一个队列，完成队列的 Push 和 Pop 操作。

**解题思路**

in 栈用来处理入栈（push）操作，out 栈用来处理出栈（pop）操作。一个元素进入 in 栈之后，出栈的顺序被反转。当元素要出栈时，需要先进入 out 栈，此时元素出栈顺序再一次被反转，因此出栈顺序就和最开始入栈顺序是相同的，先进入的元素先退出，这就是队列的顺序。

```java
Stack<Integer> in = new Stack<Integer>();
Stack<Integer> out = new Stack<Integer>();

public void push(int node) {
    in.push(node);
}

public int pop() throws Exception {
    //要等stack2空了在往里面添加元素,stack1里面的元素会压在
    //stack2内部的元素上面,破坏了先进先出规则
    if (out.isEmpty())
        while (!in.isEmpty())
            out.push(in.pop());

    if (out.isEmpty())
        throw new Exception("queue is empty");

    return out.pop();
}
```

# 10.1 斐波那契数列

[NowCoder](https://www.nowcoder.com/practice/c6c7742f5ba7442aada113136ddea0c3?tpId=13&tqId=11160&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

求斐波那契数列的第 n 项，n <= 39。

<div align="center"><img src="https://latex.codecogs.com/gif.latex?f(n)=\left\{\begin{array}{rcl}0&&{n=0}\\1&&{n=1}\\f(n-1)+f(n-2)&&{n>1}\end{array}\right." class="mathjax-pic"/></div> <br>
**解题思路**

如果使用递归求解，会重复计算一些子问题。例如，计算 f(4) 需要计算 f(3) 和 f(2)，计算 f(3) 需要计算 f(2) 和 f(1)，可以看到 f(2) 被重复计算了。
<div align="center"> <img src="pics/c13e2a3d-b01c-4a08-a69b-db2c4e821e09.png" width="350px"/> </div><br>
递归是将一个问题划分成多个子问题求解，动态规划也是如此，但是动态规划会把子问题的解缓存起来，从而避免重复求解子问题。

```java
 public int Fibonacci(int n) {
        int[] fibs = calFib();
        return fibs[n];
    }

    //把之前计算的fib数列存储起来,既可以防止重复计算,
    //又可以在取值的时候迅速拿到值
    private int[] calFib() {
        int[] fibs = new int[40];
        fibs[1] = 1;
        for (int i = 2; i < 40; i++) {
            fibs[i] = fibs[i - 1] + fibs[i - 2];
        }
        return fibs;
    }
```

# 10.2 矩形覆盖

[NowCoder](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形，总共有多少种方法？

<div align="center"> <img src="pics/b903fda8-07d0-46a7-91a7-e803892895cf.gif" width="100px"> </div><br>
**解题思路**

当 n 为 1 时，只有一种覆盖方法：

<div align="center"> <img src="pics/f6e146f1-57ad-411b-beb3-770a142164ef.png" width="100px"> </div><br>
当 n 为 2 时，有两种覆盖方法：

<div align="center"> <img src="pics/fb3b8f7a-4293-4a38-aae1-62284db979a3.png" width="200px"> </div><br>
要覆盖 2\*n 的大矩形，可以先覆盖 2\*1 的矩形，再覆盖 2\*(n-1) 的矩形；或者先覆盖 2\*2 的矩形，再覆盖 2\*(n-2) 的矩形。

**2\*2有两种覆盖方法,但其中一种和2\*1重合**

而覆盖 2\*(n-1) 和 2\*(n-2) 的矩形可以看成子问题。该问题的递推公式如下：

<!-- <div align="center"><img src="https://latex.codecogs.com/gif.latex?f(n)=\left\{\begin{array}{rcl}1&&{n=1}\\2&&{n=2}\\f(n-1)+f(n-2)&&{n>1}\end{array}\right." class="mathjax-pic"/></div> <br> -->

<div align="center"> <img src="pics/508c6e52-9f93-44ed-b6b9-e69050e14807.jpg" width="350px"> </div><br>
非递归写法:
```java
  public int RectCover(int target) {
        int pre1 = 1;
        int pre2 = 2;
        if (target <= 2) {
            return target;
        }
        int result = -1;
        for (int i = 3; i <= target; i++) {
            result = pre1 + pre2;
            pre1 = pre2;
            pre2 = result;
        }
        return result;
    }
```
递归写法:
```java
public int RectCover2(int target) {
        if (target < 1) {
            return 0;
        } else if (target == 1 || target == 2) {
            return target;
        } else {
            return RectCover(target-1) + RectCover(target-2);
        }
    }
```

# 10.3 跳台阶

[NowCoder](https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=13&tqId=11161&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

一只青蛙一次可以跳上 1 级台阶，也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

<div align="center"> <img src="pics/9dae7475-934f-42e5-b3b3-12724337170a.png" width="380px"> </div><br>
**解题思路**

当 n = 1 时，只有一种跳法：

<div align="center"> <img src="pics/72aac98a-d5df-4bfa-a71a-4bb16a87474c.png" width="250px"> </div><br>
当 n = 2 时，有两种跳法：

<div align="center"> <img src="pics/1b80288d-1b35-4cd3-aa17-7e27ab9a2389.png" width="300px"> </div><br>
跳 n 阶台阶，可以先跳 1 阶台阶，再跳 n-1 阶台阶；或者先跳 2 阶台阶，再跳 n-2 阶台阶。而 n-1 和 n-2 阶台阶的跳法可以看成子问题，该问题的递推公式为：

<div align="center"> <img src="pics/508c6e52-9f93-44ed-b6b9-e69050e14807.jpg" width="350px"> </div><br>
递归写法:
```java
 public int JumpFloor(int target) {
        if (target == 0) {
            return 0;
        } else if (target == 1 || target == 2) {
            return target;
        } else {
            return JumpFloor(target - 1) + JumpFloor(target - 2);
        }
    }
```
非递归写法
```java
public int JumpFloor(int target) {
        if (target <= 2) {
            return target;
        } else {
            int pre1 = 1;
            int pre2 = 2;
            int result = 0;
            for (int i = 3; i <= target; i++) {
                result = pre1 + pre2;
                pre1 = pre2;
                pre2 = result;
            }
            return result;
        }
    }
```

# 10.4 变态跳台阶

[NowCoder](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&tqId=11162&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

一只青蛙一次可以跳上 1 级台阶，也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

<div align="center"> <img src="pics/cd411a94-3786-4c94-9e08-f28320e010d5.png" width="380px"> </div><br>
**解题思路**

动态规划

```java
public int JumpFloorII(int target) {
        if (target <= 0) {
            return 0;
        }
        if (target <= 2) {
            return target;
        }
        int[] array = new int[target + 1];
        array[1] = 1;
        array[2] = 2;

        for (int i = 3; i <= target; i++) {
            for (int j = 1; j < i; j++) {
                array[i] = array[i] + array[j];
            }
            array[i] = array[i] + 1;
        }
        return array[target];
    }
```
数学推导

跳上 n-1 级台阶，可以从 n-2 级跳 1 级上去，也可以从 n-3 级跳 2 级上去...，那么

```
f(n-1) = f(n-2) + f(n-3) + ... + f(0)
```

同样，跳上 n 级台阶，可以从 n-1 级跳 1 级上去，也可以从 n-2 级跳 2 级上去... ，那么

```
f(n) = f(n-1) + f(n-2) + ... + f(0)
```

综上可得

```
f(n) - f(n-1) = f(n-1)
```

即

```
f(n) = 2*f(n-1)
```

所以 f(n) 是一个等比数列

```java
public int JumpFloorII(int target) {
    //返回值强转,底数与指数,不用导包
    return (int) Math.pow(2, target - 1);
}
```

# 11. 旋转数组的最小数字

[NowCoder](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&tqId=11159&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)

**题目描述**

把一个数组最开始的若干个元素搬到数组的末尾，我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转，输出旋转数组的最小元素。

**解题思路**

将旋转数组对半分可以得到一个包含最小元素的新旋转数组，以及一个非递减排序的数组。新的旋转数组的数组元素是原数组的一半，从而将问题规模减少了一半，这种折半性质的算法的时间复杂度为 O(logN)（为了方便，这里将 log<sub>2</sub>N 写为 logN）。

此时问题的关键在于确定对半分得到的两个数组哪一个是旋转数组，哪一个是非递减数组。我们很容易知道非递减数组的第一个元素一定小于等于最后一个元素。

通过修改二分查找算法进行求解（l 代表 low，m 代表 mid，h 代表 high）：

- 当 nums[m] <= nums[h] 时，表示 [m, h] 区间内的数组是非递减数组，[l, m] 区间内的数组是旋转数组，此时令 h = m；
- 否则 [m + 1, h] 区间内的数组是旋转数组，令 l = m + 1。

```java
    public int minNumberInRotateArray(int[] array) {

        if (array.length <= 0) {
            return 0;
        }
        int l = 0;
        int h = array.length - 1;
        while (l < h) {
            int m = l + (h - l) / 2;
            if (array[l] == array[m] && array[m] == array[h]) {
                return orderFind(array, l, h);
            } else if (array[m] <= array[h]) {
                h = m;
            } else {
                l = m + 1;
            }
        }
        return array[l];
    }

    private int orderFind(int[] array, int l, int h) {
        if (array.length <= 0) {
            return 0;
        }
        for (int i = l; i < h; i++) {
            if (array[i] > array[i + 1]) {
                return array[i + 1];
            }
        }
        return array[l];
    }
}

改进:
整体有序要考虑二分查找
针对此题的边界的解决办法:

			为什么要返回array[l];因为int m = l + (h - l) / 2;使得m偏向于l
 			if (array[l] == array[m] && array[m] == array[h]) {
                return orderFind(array, l, h);
            } else if (array[m] <= array[h]) {
                h = m;
            } else {
                l = m + 1;
            }
			 return array[l];

import java.util.ArrayList;
public class Solution {
    public int minNumberInRotateArray(int [] array) {
        if(array.length==0){
            return 0;
        }

        for(int i = 0;i<array.length-1;i++){
            if(array[i]>array[i+1]){
                return array[i+1];
            }
        }
        return array[0];
    }
}
```

# 12. 矩阵中的路径

[NowCoder](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始，每一步可以在矩阵中向上下左右移动一个格子。如果一条路径经过了矩阵中的某一个格子，则该路径不能再进入该格子。

例如下面的矩阵包含了一条 bfce 路径。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1db1c7ea-0443-478b-8df9-7e33b1336cc4.png" width="200px"> </div><br>
**解题思路**

使用回溯法（backtracking）进行求解，它是一种暴力搜索方法，通过搜索所有可能的结果来求解问题。回溯法在一次搜索结束时需要进行回溯（回退），将这一次搜索过程中设置的状态进行清除，从而开始一次新的搜索过程。例如下图示例中，从 f 开始，下一步有 4 种搜索可能，如果先搜索 b，需要将 b 标记为已经使用，防止重复使用。在这一次搜索结束之后，需要将 b 的已经使用状态清除，并搜索 c。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/dc964b86-7a08-4bde-a3d9-e6ddceb29f98.png" width="200px"> </div><br>
本题的输入是数组而不是矩阵（二维数组），因此需要先将数组转换成矩阵。

```java
private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
private int rows;
private int cols;

public boolean hasPath(char[] array, int rows, int cols, char[] str) {
    if (rows == 0 || cols == 0) return false;
    this.rows = rows;
    this.cols = cols;
    boolean[][] marked = new boolean[rows][cols];
    char[][] matrix = buildMatrix(array);
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            if (backtracking(matrix, str, marked, 0, i, j))
                return true;

    return false;
}

private boolean backtracking(char[][] matrix, char[] str,
                             boolean[][] marked, int pathLen, int r, int c) {

    if (pathLen == str.length) return true;
    if (r < 0 || r >= rows || c < 0 || c >= cols
            || matrix[r][c] != str[pathLen] || marked[r][c]) {

        return false;
    }
    marked[r][c] = true;
    for (int[] n : next)
        if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1]))
            return true;
    marked[r][c] = false;
    return false;
}

private char[][] buildMatrix(char[] array) {
    char[][] matrix = new char[rows][cols];
    for (int r = 0, idx = 0; r < rows; r++)
        for (int c = 0; c < cols; c++)
            matrix[r][c] = array[idx++];
    return matrix;
}

改进:
backTracing里面,返回的条件是str.length==pathLen;
终止的条件有:row<0,row>rows,col<0,col>cols,matrix[i][j]!=str[pathLen]和marked[i][j]==false
marked记得设置成true和false;

public class Solution {
    private int[][] next = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    private int rows, cols;

    public boolean hasPath(char[] array, int rows, int cols, char[] str) {
        this.rows = rows;
        this.cols = cols;
        //制作矩阵
        boolean[][] marked = new boolean[rows][cols];
        char[][] matrix = buildMatrix(array, rows, cols);

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (backTracing(matrix, marked, i, j, str, 0))
                    return true;
            }
        }
        return false;
        //回溯
    }

    private boolean backTracing(char[][] matrix, boolean[][] marked, int row,
                                int col, char[] str, int pathLen) {
        if (pathLen == str.length) {
            return true;
        }
        if (row < 0 || row >= rows || col < 0 || col >= cols || matrix[row][col] != str[pathLen] || marked[row][col]) {
            return false;
        }
        marked[row][col] = true;
        for (int[] temp : next) {
            if (backTracing(matrix, marked, row + temp[0], col + temp[1], str, pathLen + 1)) {
                return true;
            }
        }
        marked[row][col] = false;
        return false;

    }

    private char[][] buildMatrix(char[] array, int rows, int cols) {
        char[][] matrix = new char[rows][cols];
        for (int i = 0, idx = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                matrix[i][j] = array[idx++];
            }
        }
        return matrix;
    }

}
```

# 13. 机器人的运动范围

[NowCoder](https://www.nowcoder.com/practice/6e5207314b5241fb83f2329e89fdecc8?tpId=13&tqId=11219&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动，每一次只能向左右上下四个方向移动一格，但是不能进入行坐标和列坐标的数位之和大于 k 的格子。

例如，当 k 为 18 时，机器人能够进入方格 (35,37)，因为 3+5+3+7=18。但是，它不能进入方格 (35,38)，因为 3+5+3+8=19。请问该机器人能够达到多少个格子？

**解题思路**

使用深度优先搜索（Depth First Search，DFS）方法进行求解。回溯是深度优先搜索的一种特例，它在一次搜索过程中需要设置一些本次搜索过程的局部状态，并在本次搜索结束之后清除状态。而普通的深度优先搜索并不需要使用这些局部状态，虽然还是有可能设置一些全局状态。

```java
private static final int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
private int cnt = 0;
private int rows;
private int cols;
private int threshold;
private int[][] digitSum;

public int movingCount(int threshold, int rows, int cols) {
    this.rows = rows;
    this.cols = cols;
    this.threshold = threshold;
    initDigitSum();
    boolean[][] marked = new boolean[rows][cols];
    dfs(marked, 0, 0);
    return cnt;
}

private void dfs(boolean[][] marked, int r, int c) {
    if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c])
        return;
    marked[r][c] = true;
    if (this.digitSum[r][c] > this.threshold)
        return;
    cnt++;
    for (int[] n : next)
        dfs(marked, r + n[0], c + n[1]);
}

private void initDigitSum() {
    int[] digitSumOne = new int[Math.max(rows, cols)];
    for (int i = 0; i < digitSumOne.length; i++) {
        int n = i;
        while (n > 0) {
            digitSumOne[i] += n % 10;
            n /= 10;
        }
    }
    this.digitSum = new int[rows][cols];
    for (int i = 0; i < this.rows; i++)
        for (int j = 0; j < this.cols; j++)
            this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j];
}

改进:
制作一个每个坐标的求和矩阵,方便和threshold做比较
marked要传入bfs里面

public class Solution {
    private int[][] locationSum;
    private int rows, cols;
    private int threshold;
    private boolean[][] marked;
    private int count = 0;
    private int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};

    public int movingCount(int threshold, int rows, int cols) {
        this.threshold = threshold;
        this.cols = cols;
        this.rows = rows;
        marked = new boolean[rows][cols];
        locationSum = getLocationSum(rows, cols);
         //进入bfs
        bfs(marked, 0, 0);
        return count;
    }

    //bfs  不符合条件就退出,符合就继续往下走,走过的就不能走了(不走回头路)
    private void bfs(boolean[][] marked, int row, int col) {
        if (row < 0 || col < 0 || row >= rows || col >= cols || marked[row][col] || locationSum[row][col] > threshold) {
            return;
        }
        count++;
        marked[row][col] = true;
        for (int[] temp : next) {
            bfs(marked, row + temp[0], col + temp[1]);
        }
        return;
    }
    //制作每个方格的和
    private int[][] getLocationSum(int rows, int cols) {
        int maxLen = Math.max(rows, cols);
        int[] sumOne = new int[maxLen];
        for (int i = 0; i < maxLen; i++) {
            int sum = 0;
            int loc = i;
            while (loc > 0) {
                sum += loc % 10;
                loc = loc / 10;
            }
            sumOne[i] = sum;
        }
        int[][] sumTwo = new int[maxLen][maxLen];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                sumTwo[i][j] = sumOne[i] + sumOne[j];
            }
        }
        return sumTwo;
    }
}
```

# @14. 剪绳子

[Leetcode](https://leetcode.com/problems/integer-break/description/)

[NowCode](https://www.nowcoder.com/questionTerminal/57d85990ba5b440ab888fc72b0751bf8?answerType=1&f=discussion)

**题目描述**

把一根绳子剪成多段，并且使得每段的长度乘积最大。

```html
n = 2
return 1 (2 = 1 + 1)

n = 10
return 36 (10 = 3 + 3 + 4)
```

**解题思路**

贪心

尽可能多剪长度为 3 的绳子，并且不允许有长度为 1 的绳子出现。如果出现了，就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合，把它们切成两段长度为 2 的绳子。

证明：当 n >= 5 时，3(n - 3) - n = 2n - 9 > 0，且 2(n - 2) - n = n - 4 > 0。因此在 n >= 5 的情况下，将绳子剪成一段为 2 或者 3，得到的乘积会更大。又因为 3(n - 3) - 2(n - 2) = n - 5 >= 0，所以剪成一段长度为 3 比长度为 2 得到的乘积更大。

```java
public int integerBreak(int n) {
    if (n < 2)
        return 0;
    if (n == 2)
        return 1;
    if (n == 3)
        return 2;
    int timesOf3 = n / 3;
    if (n - timesOf3 * 3 == 1)
        timesOf3--;
    int timesOf2 = (n - timesOf3 * 3) / 2;
    return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2));
}
```

动态规划

```java
public int integerBreak(int n) {
    int[] dp = new int[n + 1];
    dp[1] = 1;
    for (int i = 2; i <= n; i++)
        for (int j = 1; j =0;j--  还是 j=1;j<i;j++都是没有区别的

```

# 15. 二进制中 1 的个数

[NowCoder](https://www.nowcoder.com/practice/8ee967e43c2c4ec193b040ea7fbb10b8?tpId=13&tqId=11164&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入一个整数，输出该数二进制表示中 1 的个数。

**n&(n-1)**

该位运算去除 n 的位级表示中最低的那一位。

```
n       : 10110100
n-1     : 10110011
n&(n-1) : 10110000
```

时间复杂度：O(M)，其中 M 表示 1 的个数。

```java
public int NumberOf1(int n) {
    int cnt = 0;
    while (n != 0) {
        cnt++;
        n &= (n - 1);
    }
    return cnt;
}

思考:
n&(n-1)该位运算去除 n 的位级表示中最低的那一位。
    而且题目中的整数包含负数,所while(里面是n!=0 而不是n>0)
```

**Integer.bitCount()**

```java
public int NumberOf1(int n) {
    return Integer.bitCount(n);
}
```

# @16. 数值的整数次方

@因为 (x\*x)n/2 可以通过递归求解，并且每次递归 n 都减小一半，因此整个算法的时间复杂度为 O(logN)。
    如果for循环来求n遍的话,复杂度为O(N)

@考虑 指数是负数,底数是负数,指数是偶数奇数的情况.

[NowCoder](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent，求 base 的 exponent 次方。

**解题思路**

下面的讨论中 x 代表 base，n 代表 exponent。

<!--<div align="center"><img src="https://latex.codecogs.com/gif.latex?x^n=\left\{\begin{array}{rcl}(x*x)^{n/2}&&{n\%2=0}\\x*(x*x)^{n/2}&&{n\%2=1}\end{array}\right." class="mathjax-pic"/></div> <br>-->

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/48b1d459-8832-4e92-938a-728aae730739.jpg" width="330px"> </div><br>
因为 (x\*x)<sup>n/2</sup> 可以通过递归求解，并且每次递归 n 都减小一半，因此整个算法的时间复杂度为 O(logN)。

```java
public double Power(double base, int exponent) {
    if (exponent == 0)
        return 1;
    if (exponent == 1)
        return base;
    boolean isNegative = false;
    if (exponent < 0) {
        exponent = -exponent;
        isNegative = true;
    }
    double pow = Power(base * base, exponent / 2);
    if (exponent % 2 != 0)
        pow = pow * base;
    return isNegative ? 1 / pow : pow;
}

改进:

```

# @17. 打印从 1 到最大的 n 位数

@回溯而不是从1到n打印.

**题目描述**

输入数字 n，按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3，则打印出 1、2、3 一直到最大的 3 位数即 999。

**解题思路**

由于 n 可能会非常大，因此不能直接用 int 表示数字，而是用 char 数组进行存储。

使用回溯法得到所有的数。

```java
public void print1ToMaxOfNDigits(int n) {
    if (n <= 0)
        return;
    char[] number = new char[n];
    print1ToMaxOfNDigits(number, 0);
}

private void print1ToMaxOfNDigits(char[] number, int digit) {
    if (digit == number.length) {
        printNumber(number);
        return;
    }
    for (int i = 0; i < 10; i++) {
        number[digit] = (char) (i + '0');
        print1ToMaxOfNDigits(number, digit + 1);
    }
}

private void printNumber(char[] number) {
    int index = 0;
    while (index < number.length && number[index] == '0')
        index++;
    while (index < number.length)
        System.out.print(number[index++]);
    System.out.println();
}
```

# 18.1 在 O(1) 时间内删除链表节点

**解题思路**

① 如果该节点不是尾节点，那么可以直接将下一个节点的值赋给该节点，然后令该节点指向下下个节点，再删除下一个节点，时间复杂度为 O(1)。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1176f9e1-3442-4808-a47a-76fbaea1b806.png" width="600"/> </div><br>
② 否则，就需要先遍历链表，找到节点的前一个节点，然后让前一个节点指向 null，时间复杂度为 O(N)。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4bf8d0ba-36f0-459e-83a0-f15278a5a157.png" width="600"/> </div><br>
综上，如果进行 N 次操作，那么大约需要操作节点的次数为 N-1+N=2N-1，其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数，N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N \~ 2，因此该算法的平均时间复杂度为 O(1)。

```java
public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
    if (head == null || tobeDelete == null)
        return null;
    if (tobeDelete.next != null) {
        // 要删除的节点不是尾节点
        ListNode next = tobeDelete.next;
        tobeDelete.val = next.val;
        tobeDelete.next = next.next;
    } else {
        if (head == tobeDelete)
             // 只有一个节点
            head = null;
        else {
            ListNode cur = head;
            while (cur.next != tobeDelete)
                cur = cur.next;
            cur.next = null;
        }
    }
    return head;
}
```

# 18.2 删除链表中重复的结点

[NowCoder](https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/17e301df-52e8-4886-b593-841a16d13e44.png" width="450"/> </div><br>
**解题描述**

```java
public ListNode deleteDuplication(ListNode pHead) {
    if (pHead == null || pHead.next == null)
        return pHead;
    ListNode next = pHead.next;
    if (pHead.val == next.val) {
        while (next != null && pHead.val == next.val)
            next = next.next;
        return deleteDuplication(next); //返回的是从next开始的链表,所以pHead如果也是重复值,pHead也会被删掉
    } else {
        pHead.next = deleteDuplication(pHead.next);
        return pHead;
    }
}

改进:
上面递归的方法比较绕,建议用下面的办法,精髓在于创建了一个头结点Head,在pHead也是重复元素的情况下,可以删除Head下的子节点pHead.

    各种next比较绕,要重新做几遍
  public ListNode deleteDuplication(ListNode pHead) {
        ListNode Head = new ListNode(0);
        Head.next = pHead;
        ListNode pre = Head;
        ListNode next = pre.next;
        while (next != null) {
            if (next.next != null && next.val == next.next.val) {
                while (next.next != null && next.val == next.next.val) {
                    next = next.next;
                }
                pre.next = next.next;
                next = next.next;
            } else {
                pre = next;
                next = next.next;
            }
        }
        return Head.next;
    }
```

# @@@19. 正则表达式匹配

[NowCoder](https://www.nowcoder.com/practice/45327ae22b7b413ea21df13ee7d6429c?tpId=13&tqId=11205&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符，而 '\*' 表示它前面的字符可以出现任意次（包含 0 次）。

在本题中，匹配是指字符串的所有字符匹配整个模式。例如，字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配，但是与 "aa.a" 和 "ab\*a" 均不匹配。

**解题思路**

应该注意到，'.' 是用来当做一个任意字符，而 '\*' 是用来重复前面的字符。这两个的作用不同，不能把 '.' 的作用和 '\*' 进行类比，从而把它当成重复前面字符一次。

```java
 public boolean match(char[] str, char[] pattern) {
        String s = new String(str);
        String p = new String(pattern);
        if (s == null || p == null) {
            return false;
        }
        int len1 = s.length();
        int len2 = p.length();
        boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
        dp[0][0] = true;
        //初始化"" 与p的匹配关系  "" 和 a*a*a*是可以匹配的
        for (int i = 0; i < len2; i++) {
            if (p.charAt(i) == '*' && dp[0][i - 1]) {
                dp[0][i + 1] = true;
            }
        }
        for (int i = 0; i < len1; i++) {
            for (int j = 0; j < len2; j++) {
                // ##s   ##p     与     ##s    ##.
                if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') {
                    dp[i + 1][j + 1] = dp[i][j];
                }
                if (p.charAt(j) == '*') {
                    if (p.charAt(j - 1) == s.charAt(i) || p.charAt(j - 1) == '.') {
                        // ##cp    ##cpp*  i  和 j-2   p*匹配0个
                        //##p     ##p*    i-1  和  j-2   p*  匹配1个
                        //##ppp   ##p*    i-1 和  j   p* 匹配多个
                        dp[i + 1][j + 1] = dp[i + 1][j - 1] || dp[i ][j-1] || dp[i][j + 1];
                    } else {
                        // ##c  要和  ##p*匹配   i  和  j-2
                        dp[i + 1][j + 1] = dp[i + 1][j - 1];
                    }
                }
            }
        }
        return dp[len1][len2];
    }

改进:
用下面的思路 比较清晰

当模式中的第二个字符不是“*”时：
1、如果字符串第一个字符和模式中的第一个字符相匹配，那么字符串和模式都后移一个字符，然后匹配剩余的。
2、如果 字符串第一个字符和模式中的第一个字符相不匹配，直接返回false。

而当模式中的第二个字符是“*”时：
如果字符串第一个字符跟模式第一个字符不匹配，则模式后移2个字符，继续匹配。如果字符串第一个字符跟模式第一个字符匹配，可以有3种匹配方式：
1、模式后移2字符，相当于x*被忽略；
2、字符串后移1字符，模式后移2字符；
3、字符串后移1字符，模式不变，即继续匹配字符下一位，因为*可以匹配多位；

这里需要注意的是：Java里，要时刻检验数组是否越界。

链接：https://www.nowcoder.com/questionTerminal/45327ae22b7b413ea21df13ee7d6429c?f=discussion
来源：牛客网

public class Solution {
    public boolean match(char[] str, char[] pattern) {
    if (str == null || pattern == null) {
        return false;
    }
    int strIndex = 0;
    int patternIndex = 0;
    return matchCore(str, strIndex, pattern, patternIndex);
}

public boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) {
    //有效性检验：str到尾，pattern到尾，匹配成功
    if (strIndex == str.length && patternIndex == pattern.length) {
        return true;
    }
    //pattern先到尾，匹配失败
    if (strIndex != str.length && patternIndex == pattern.length) {
        return false;
    }
    //模式第2个是*，且字符串第1个跟模式第1个匹配,分3种匹配模式；如不匹配，模式后移2位
    if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
        if ((strIndex != str.length && pattern[patternIndex] == str[strIndex]) || (pattern[patternIndex] == '.' && strIndex != str.length)) {
            return matchCore(str, strIndex, pattern, patternIndex + 2)//模式后移2，视为x*匹配0个字符
                    || matchCore(str, strIndex + 1, pattern, patternIndex + 2)//视为模式匹配1个字符
                    || matchCore(str, strIndex + 1, pattern, patternIndex);//*匹配1个，再匹配str中的下一个
        } else {
            return matchCore(str, strIndex, pattern, patternIndex + 2);
        }
    }
    //模式第2个不是*，且字符串第1个跟模式第1个匹配，则都后移1位，否则直接返回false
    if ((strIndex != str.length && pattern[patternIndex] == str[strIndex]) || (pattern[patternIndex] == '.' && strIndex != str.length)) {
        return matchCore(str, strIndex + 1, pattern, patternIndex + 1);
    }
    return false;
    }
}
```

# @20. 表示数值的字符串

@index == len

@ otherWord

@分三段判  ` ***.***E***`

@count<=0

@首数字为0(小数部分可以,其他两段不可以,但本题没有这个限制)

[NowCoder](https://www.nowcoder.com/practice/6f8c901d091949a5837e24bb82a731f2?tpId=13&tqId=11206&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

```
true

"+100"
"5e2"
"-123"
"3.1416"
"-1E-16"
```

```
false

"12e"
"1a3.14"
"1.2.3"
"+-5"
"12e+4.3"
```

**解题思路**

使用正则表达式进行匹配。

```html
[]  ： 字符集合
()  ： 分组
?   ： 重复 0 ~ 1 次
+   ： 重复 1 ~ n 次
*   ： 重复 0 ~ n 次
.   ： 任意字符
\\. ： 转义后的 .
\\d ： 数字
```

```java
思路1:

	private int index = 0;
    private int len = 0;
    private boolean hasOtherWord = false;

    public boolean isNumeric(char[] str) {
        if (str == null || str.length == 0)
            return false;
        len = str.length;
        int count1 = 0, count2 = 0, count3 = 0;
        count1 = judgeNum(str);
        if (index < len && str[index] == '.') {
            index++;
            count2 = judgeUnsignNum(str);
            if (count2 < 1)
                return false;
        }
        if (index < len && (str[index] == 'E' || str[index] == 'e')) {
            index++;
            count3 = judgeNum(str);
            if (count3 < 1)
                return false;
        }
        return !hasOtherWord && index == len;
    }

    private int judgeNum(char[] str) {
        if (index >= len) {
            return 0;
        }
        if (str[index] == '+' || str[index] == '-') {
            index++;
        }
        return judgeUnsignNum(str);
    }

    private int judgeUnsignNum(char[] str) {
        int start = index;
        while (index < len) {
            if (str[index] >= '0' && str[index] <= '9') {
                index++;
            } else if (str[index] == '.' || str[index] == 'e' || str[index] == 'E') {
                return index - start;
            } else {
                hasOtherWord = true;
                return -1;
            }
        }
        return index - start;
    }

思路2:
public boolean isNumeric(char[] str) {
    if (str == null || str.length == 0)
        return false;
    return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
}
```

# @21. 调整数组顺序使奇数位于偶数前面

@创建一个新数组，时间复杂度 O(N)，空间复杂度 O(N)。

[NowCoder](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

需要保证奇数和奇数，偶数和偶数之间的相对位置不变，这和书本不太一样。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d03a2efa-ef19-4c96-97e8-ff61df8061d3.png" width="200px"> </div><br>
**解题思路**

方法一：创建一个新数组，时间复杂度 O(N)，空间复杂度 O(N)。

```java
public void reOrderArray(int[] nums) {
    // 奇数个数
    int oddCnt = 0;
    for (int x : nums)
        if (!isEven(x))
            oddCnt++;
    int[] copy = nums.clone();
    int i = 0, j = oddCnt;
    for (int num : copy) {
        if (num % 2 == 1)
            nums[i++] = num;
        else
            nums[j++] = num;
    }
}

private boolean isEven(int x) {
    return x % 2 == 0;
}

改进 :
没改进,熟悉了一下LinkedList而已

public void reOrderArray(int[] array) {
        LinkedList<Integer> oddList = new LinkedList<Integer>();
        LinkedList<Integer> evenList = new LinkedList<Integer>();
        for (int i = 0; i < array.length; i++) {
            if (isOdd(array[i])) {
                oddList.add(array[i]);
                continue;
            }
            evenList.add(array[i]);
        }
        int index = 0;
        for (Integer temp : oddList) {
            array[index++] = temp;
        }
        for (Integer temp : evenList) {
            array[index++] = temp;
        }
        return;
    }

    //奇数判断
    private boolean isOdd(int num) {
        if (num % 2 == 1) {
            return true;
        }
        return false;
    }
```

方法二：使用冒泡思想，每次都当前偶数上浮到当前最右边。时间复杂度 O(N<sup>2</sup>)，空间复杂度 O(1)，时间换空间。

```java
public void reOrderArray(int[] nums) {
    int N = nums.length;
    for (int i = N - 1; i > 0; i--) {
        for (int j = 0; j < i; j++) {
            if (isEven(nums[j]) && !isEven(nums[j + 1])) {
                swap(nums, j, j + 1);
            }
        }
    }
}

private boolean isEven(int x) {
    return x % 2 == 0;
}

private void swap(int[] nums, int i, int j) {
    int t = nums[i];
    nums[i] = nums[j];
    nums[j] = t;
}
改进
    第一个循环是 n-1次循环即可
    第二个循环是  第一次:j为 0~n-2 的array[j]与array[j+1]作比较
        		第二次: j为 0~n-3 的array[j]与array[j+1]作比较

```

# 22. 链表中倒数第 K 个结点

[NowCoder](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

输入一个链表，输出该链表中倒数第k个结点

**解题思路**

设链表的长度为 N。设置两个指针 P1 和 P2，先让 P1 移动 K 个节点，则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动，可以知道当 P1 移动到链表结尾时，P2 移动到第 N - K 个节点处，该位置就是倒数第 K 个节点。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6b504f1f-bf76-4aab-a146-a9c7a58c2029.png" width="500"/> </div><br>
```java
public ListNode FindKthToTail(ListNode head, int k) {
    if (head == null)
        return null;
    ListNode P1 = head;
    while (P1 != null && k-- > 0)
        P1 = P1.next;
    if (k > 0)
        return null;
    ListNode P2 = head;
    while (P1 != null) {
        P1 = P1.next;
        P2 = P2.next;
    }
    return P2;
}

改进:没啥好改进的
public ListNode FindKthToTail(ListNode head, int k) {
        //p1 先走k
        //p2 和 p1 一起走
        //返回p2
        ListNode p1 = head;
        ListNode p2 = head;
        for (int i = 0; i < k; i++) {
            if (p1 == null) {
                return null;
            }
            p1 = p1.next;
        }
        while (p1 != null) {
            p1 = p1.next;
            p2 = p2.next;
        }
        return p2;
    }
```

# @@@@@23. 链表中环的入口结点

@第一步  先  快指针 2个  慢指针1个  再 判断是否相遇

@第二步 快指针和慢指针都1个    在判断是否相遇.

[NowCoder](https://www.nowcoder.com/practice/253d2c59ec3e4bc68da16833f79a38e4?tpId=13&tqId=11208&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

一个链表中包含环，请找出该链表的环的入口结点。要求不能使用额外的空间。

**解题思路**

使用双指针，一个指针 fast 每次移动两个节点，一个指针 slow 每次移动一个节点。因为存在环，所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置，此时 fast 移动的节点数为 x+y+z+y，slow 为 x+y，由于 fast 速度比 slow 快一倍，因此 x+2y+z=2(x+y)，得到 x=z。

在相遇点，slow 要到环的入口点还需要移动 z 个节点，如果让 fast 重新从头开始移动，并且速度变为每次移动一个节点，那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z，因此 fast 和 slow 将在环入口点相遇。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/bb7fc182-98c2-4860-8ea3-630e27a5f29f.png" width="500"/> </div><br>
```java

public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }

        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && slow != null) {

            fast = fast.next;
            if (fast != null) {
                fast = fast.next;
            }
            slow = slow.next;
            if (fast != null && fast == slow) {
                fast = head;
                break;
            }
        }

        while (fast != null && slow != null) {
            if (fast == slow) {
                return fast;
            }
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }

public ListNode EntryNodeOfLoop(ListNode pHead) {
    if (pHead == null || pHead.next == null)
        return null;
    ListNode slow = pHead, fast = pHead;
    do {
        fast = fast.next.next;
        slow = slow.next;
    } while (slow != fast);
    fast = pHead;
    while (slow != fast) {
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}

改进:
	记住环形入口的结论
    记住代码
    本题没有包含不含环的情况,代码鲁棒性不行
```

# 24. 反转链表

[NowCoder](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**解题思路**

递归

```java
public ListNode ReverseList(ListNode head) {
    if (head == null || head.next == null)
        return head;
    ListNode next = head.next;
    head.next = null;
    ListNode newHead = ReverseList(next);
    next.next = head;
    return newHead;
}
```

迭代

使用头插法。

```java
public ListNode ReverseList(ListNode head) {
    ListNode newList = new ListNode(-1);
    while (head != null) {
        ListNode next = head.next;
        head.next = newList.next;
        newList.next = head;
        head = next;
    }
    return newList.next;
}

改进:
先判断head==null  来保证  ListNode next = head.next;不会报空指针

 public ListNode ReverseList(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode reverseHead = new ListNode(-1);
        ListNode node = head;
        ListNode next = head.next;
        while (node != null) {
            next = node.next;
            node.next = reverseHead.next;
            reverseHead.next = node;
            node = next;
        }
        return reverseHead.next;
    }
```

# 25. 合并两个排序的链表

[NowCoder](https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c094d2bc-ec75-444b-af77-d369dfb6b3b4.png" width="400"/> </div><br>
**解题思路**

递归

```java
public ListNode Merge(ListNode list1, ListNode list2) {
    if (list1 == null)
        return list2;
    if (list2 == null)
        return list1;
    if (list1.val <= list2.val) {
        list1.next = Merge(list1.next, list2);
        return list1;
    } else {
        list2.next = Merge(list1, list2.next);
        return list2;
    }
}
```

迭代

```java
public ListNode Merge(ListNode list1, ListNode list2) {
    ListNode head = new ListNode(-1);
    ListNode cur = head;
    while (list1 != null && list2 != null) {
        if (list1.val <= list2.val) {
            cur.next = list1;
            list1 = list1.next;
        } else {
            cur.next = list2;
            list2 = list2.next;
        }
        cur = cur.next;
    }
    if (list1 != null)
        cur.next = list1;
    if (list2 != null)
        cur.next = list2;
    return head.next;
}
```

# 26. 树的子结构

[NowCoder](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/84a5b15a-86c5-4d8e-9439-d9fd5a4699a1.jpg" width="450"/> </div><br>
**解题思路**

```java
public boolean HasSubtree(TreeNode root1, TreeNode root2) {
    if (root1 == null || root2 == null)
        return false;
    return isSubtreeWithRoot(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);
}

private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) {
    if (root2 == null)
        return true;
    if (root1 == null)
        return false;
    if (root1.val != root2.val)
        return false;
    return isSubtreeWithRoot(root1.left, root2.left) && isSubtreeWithRoot(root1.right, root2.right);
}
```

# 27. 二叉树的镜像

[NowCoder](https://www.nowcoder.com/practice/564f4c26aa584921bc75623e48ca3011?tpId=13&tqId=11171&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

操作给定的二叉树，将其变换为源二叉树的镜像。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0c12221f-729e-4c22-b0ba-0dfc909f8adf.jpg" width="300"/> </div><br>
**解题思路**

```java
public void Mirror(TreeNode root) {
    if (root == null)
        return;
    swap(root);
    Mirror(root.left);
    Mirror(root.right);
}

private void swap(TreeNode root) {
    TreeNode t = root.left;
    root.left = root.right;
    root.right = t;
}
```

# 28 对称的二叉树

[NowCoder](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0c12221f-729e-4c22-b0ba-0dfc909f8adf.jpg" width="300"/> </div><br>
**解题思路**

```java
boolean isSymmetrical(TreeNode pRoot) {
    if (pRoot == null)
        return true;
    return isSymmetrical(pRoot.left, pRoot.right);
}

boolean isSymmetrical(TreeNode t1, TreeNode t2) {
    if (t1 == null && t2 == null)
        return true;
    if (t1 == null || t2 == null)
        return false;
    if (t1.val != t2.val)
        return false;
    return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left);
}
```

# 29. 顺时针打印矩阵

[NowCoder](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

下图的矩阵顺时针打印结果为：1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/48517227-324c-4664-bd26-a2d2cffe2bfe.png" width="200px"> </div><br>
**解题思路**

```java

改进:
这个思路更好,
用rowStart,rowEnd,colStart,colEnd来控制范围
注意 rowEnd = matrix.length-1;
if (rowEnd != rowStart) {//如果边界只有一行的话,那第一个for循环来打印就好了

public ArrayList<Integer> printMatrix(int[][] matrix) {
        int rowEnd = matrix.length - 1;   //
        int colEnd = matrix[0].length - 1;
        int rowStart = 0;
        int colStart = 0;

        ArrayList<Integer> ret = new ArrayList<>();
        while (rowStart <= rowEnd && colStart <= colEnd) {
            for (int i = colStart; i <= colEnd; i++) {
                ret.add(matrix[rowStart][i]);

            }
            for (int i = rowStart + 1; i <= rowEnd; i++) {
                ret.add(matrix[i][colEnd]);
            }
            if (rowEnd != rowStart) {//如果边界只有一行的话,那第一个for循环来打印就好了.
                for (int i = colEnd - 1; i >= colStart; i--) {
                    ret.add(matrix[rowEnd][i]);
                }
            }
            if (colEnd != colStart) {
                for (int i = rowEnd - 1; i >= rowStart + 1; i--) {
                    ret.add(matrix[i][colStart]);
                }
            }
            rowStart++;
            rowEnd--;
            colStart++;
            colEnd--;
        }
        return ret;
    }

改进:
上面的做法更好.

rowLocationBound = matrix.length
colLocationBound = matrix[0].length;
用layer来控制每一层
如果count>sum了,则不要再继续循环

 public ArrayList<Integer> printMatrix(int[][] matrix) {
        int colLocationBound = matrix[0].length;
        int rowLocationBound = matrix.length;
        ArrayList<Integer> ret = new ArrayList<Integer>();
        int layer = 0;//从0开始
        int count = 0;//从1开始
        int sum = rowLocationBound * colLocationBound;
        while (count < rowLocationBound * colLocationBound) {
            for (int i = layer; i < colLocationBound - layer && count < sum; i++) {
                ret.add(matrix[layer][i]);
                count++;
            }
            for (int j = layer + 1; j < rowLocationBound - 1 - layer && count < sum; j++) {
                ret.add(matrix[j][colLocationBound - layer - 1]);
                count++;
            }

            for (int i = colLocationBound - layer - 1; i >= layer && count < sum; i--) {
                ret.add(matrix[rowLocationBound - layer - 1][i]);
                count++;
            }
            for (int i = rowLocationBound - 1 - layer - 1; i >= layer + 1 && count < sum; i--) {
                ret.add(matrix[i][layer]);
                count++;
            }
            layer++;
        }
        return ret;
    }
```

# 30. 包含 min 函数的栈

[NowCoder](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

定义栈的数据结构，请在该类型中实现一个能够得到栈最小元素的 min 函数。

**解题思路**

```java
private Stack<Integer> dataStack = new Stack<>();
private Stack<Integer> minStack = new Stack<>();

public void push(int node) {
    dataStack.push(node);
    minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node));
}

public void pop() {
    dataStack.pop();
    minStack.pop();
}

public int top() {
    return dataStack.peek();
}

public int min() {
    return minStack.peek();
}
改进: minStack不能用一个记录最小值的int来替代.因为pop一次,再求最小值,int里面的值不符合要求了.
```

# @@@@31. 栈的压入、弹出序列

[NowCoder](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&tqId=11174&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入两个整数序列，第一个序列表示栈的压入顺序，请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。

例如序列 1,2,3,4,5 是某栈的压入顺序，序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列，但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。

**解题思路**

使用一个栈来模拟压入弹出操作。

```java
public boolean IsPopOrder(int[] pushSequence, int[] popSequence) {
    int n = pushSequence.length;
    Stack<Integer> stack = new Stack<>();
    for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
        stack.push(pushSequence[pushIndex]);
        while (popIndex < n && !stack.isEmpty()
                && stack.peek() == popSequence[popIndex]) {
            stack.pop();
            popIndex++;
        }
    }
    return stack.isEmpty();
}

改进
    最后一句 return 可以简化为上面return stack.isEmpty();

   public boolean IsPopOrder(int[] pushA, int[] popA) {
        int n = pushA.length;
        Stack<Integer> stack = new Stack<Integer>();
        for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
            stack.push(pushA[pushIndex]);
            while (popIndex < n && !stack.isEmpty() && stack.peek() == popA[popIndex]) {
                stack.pop();
                popIndex++;
            }
        }
        if (stack.isEmpty()) {
            return true;
        }
        return false;
    }
```

# 32.1 从上往下打印二叉树

[NowCoder](https://www.nowcoder.com/practice/7fe2212963db4790b57431d9ed259701?tpId=13&tqId=11175&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

从上往下打印出二叉树的每个节点，同层节点从左至右打印。

例如，以下二叉树层次遍历的结果为：1,2,3,4,5,6,7

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d5e838cf-d8a2-49af-90df-1b2a714ee676.jpg" width="250"/> </div><br>
**解题思路**

使用队列来进行层次遍历。

不需要使用两个队列分别存储当前层的节点和下一层的节点，因为在开始遍历一层的节点时，当前队列中的节点数就是当前层的节点数，只要控制遍历这么多节点数，就能保证这次遍历的都是当前层的节点。

```java
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
    Queue<TreeNode> queue = new LinkedList<>();
    ArrayList<Integer> ret = new ArrayList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int cnt = queue.size();
        while (cnt-- > 0) {
            TreeNode t = queue.poll();
            if (t == null)
                continue;
            ret.add(t.val);
            queue.add(t.left);
            queue.add(t.right);
        }
    }
    return ret;
}

 public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        ArrayList<Integer> list = new ArrayList<Integer>();
        if (root == null) {
            return list;
        }
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            list.add(node.val);
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }

        }
        return list;
    }
```

# 32.2 把二叉树打印成多行

[NowCoder](https://www.nowcoder.com/practice/445c44d982d04483b04a54f298796288?tpId=13&tqId=11213&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

和上题几乎一样。

**解题思路**

```java
ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
    ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(pRoot);
    while (!queue.isEmpty()) {
        ArrayList<Integer> list = new ArrayList<>();
        int cnt = queue.size();
        while (cnt-- > 0) {
            TreeNode node = queue.poll();
            if (node == null)
                continue;
            list.add(node.val);
            queue.add(node.left);
            queue.add(node.right);
        }
        if (list.size() != 0)
            ret.add(list);
    }
    return ret;
}

改进:
while (cnt-- > 0) 是最重要的一步,分割了两层树的节点

```

# 32.3 按之字形顺序打印二叉树

[NowCoder](https://www.nowcoder.com/practice/91b69814117f4e8097390d107d2efbe0?tpId=13&tqId=11212&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

请实现一个函数按照之字形打印二叉树，即第一行按照从左到右的顺序打印，第二层按照从右至左的顺序打印，第三行按照从左到右的顺序打印，其他行以此类推。

**解题思路**

```java
改进: 注意如果用队列和栈来做, 第一行正序,第二行逆序,第三行因为第二行原本就是逆序,所以顺序不好控制了
public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> listSum = new ArrayList<ArrayList<Integer>>();
        if (pRoot == null) {
            return listSum;
        }
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        int count = 1;
        queue.add(pRoot);
        while (!queue.isEmpty()) {
            ArrayList<Integer> list = new ArrayList<Integer>();
            int size = queue.size();
            while (size > 0) {
                TreeNode node = queue.poll();
                if (count % 2 == 1) {
                    list.add(node.val);
                } else {
                    list.add(0, node.val);
                }

                if (node.left != null) {
                    queue.add(node.left);
                }
                if (node.right != null) {
                    queue.add(node.right);
                }
                size--;
            }
            count++;
            listSum.add(list);
        }
        return listSum;
    }
```

# @@33. 二叉搜索树的后序遍历序列

[NowCoder](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&tqId=11176&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入一个整数数组，判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。

例如，下图是后序遍历序列 1,3,2 所对应的二叉搜索树。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/13454fa1-23a8-4578-9663-2b13a6af564a.jpg" width="150"/> </div><br>
**解题思路**

```java
public boolean VerifySquenceOfBST(int[] sequence) {
    if (sequence == null || sequence.length == 0)
        return false;
    return verify(sequence, 0, sequence.length - 1);
}

private boolean verify(int[] sequence, int first, int last) {
    if (last - first <= 1)
        return true;
    int rootVal = sequence[last];
    int cutIndex = first;
    while (cutIndex < last && sequence[cutIndex] <= rootVal)
        cutIndex++;
    for (int i = cutIndex; i < last; i++)
        if (sequence[i] < rootVal)
            return false;
    return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1);
}

改进:
寻找左右子树分界    while (cutIndex < last && sequence[cutIndex] <= rootVal)

判断右子树符不符合规则,这个很重要.
    for (int i = cutIndex; i < last; i++)
        if (sequence[i] < rootVal)
            return false;

```

# 34. 二叉树中和为某一值的路径

[NowCoder](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入一颗二叉树和一个整数，打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。

下图的二叉树有两条和为 22 的路径：10, 5, 7 和 10, 12

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ed77b0e6-38d9-4a34-844f-724f3ffa2c12.jpg" width="200"/> </div><br>
**解题思路**

```java
private ArrayList<ArrayList<Integer>> listSum = new ArrayList<ArrayList<Integer>>();
    private int sum = 0, target = 0;
    private ArrayList<Integer> path = new ArrayList<Integer>();
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
        if (root == null) {
            return listSum;
        }
        this.target = target;
        rec(root);
        return listSum;
    }

    private void rec(TreeNode root) {
        if (root == null) {
            return;
        }

        path.add(root.val);
        sum += root.val;

        if (sum == target) {
            if (root.left == null && root.right == null) {
                listSum.add(new ArrayList<Integer>(path));
            }
        }
        rec(root.left);
        rec(root.right);

        sum = sum - root.val;
        path.remove(path.size() - 1);
    }
改进
    listSum.add(path)的时候 add的是new ArrayList<>(path);  不新创建一个的话,后面paht的修改会影响listSum里面的内容
private ArrayList<ArrayList<Integer>> listSum = new ArrayList<ArrayList<Integer>>();
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
        if (root == null) {
            return listSum;
        }
        findPath(root, target, 0, new ArrayList<Integer>());
        return listSum;
    }
    private void findPath(TreeNode root, int target, int sum, ArrayList<Integer> path) {
        if (root == null) {
            return;
        }
        path.add(root.val);
        sum += root.val;
        if (sum > target) {
            path.remove(path.size() - 1);
            return;
        }
        if (sum == target && root.left == null && root.right == null) {
            listSum.add(new ArrayList<>(path));
            path.remove(path.size() - 1);
            return;
        }
        findPath(root.left, target, sum, path);
        findPath(root.right, target, sum, path);
        path.remove(path.size() - 1);
    }
```

# @@35. 复杂链表的复制

[NowCoder](https://www.nowcoder.com/practice/f836b2c43afc4b35ad6adc41ec941dba?tpId=13&tqId=11178&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入一个复杂链表（每个节点中有节点值，以及两个指针，一个指向下一个节点，另一个特殊指针指向任意一个节点），返回结果为复制后复杂链表的 head。

```java
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}
```

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/66a01953-5303-43b1-8646-0c77b825e980.png" width="300"/> </div><br>
**解题思路**

第一步，在每个节点的后面插入复制的节点。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/dfd5d3f8-673c-486b-8ecf-d2082107b67b.png" width="600"/> </div><br>
第二步，对复制节点的 random 链接进行赋值。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cafbfeb8-7dfe-4c0a-a3c9-750eeb824068.png" width="600"/> </div><br>
第三步，拆分。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e151b5df-5390-4365-b66e-b130cd253c12.png" width="600"/> </div><br>
```java
public RandomListNode Clone(RandomListNode pHead) {
    if (pHead == null)
        return null;
    // 插入新节点
    RandomListNode cur = pHead;
    while (cur != null) {
        RandomListNode clone = new RandomListNode(cur.label);
        clone.next = cur.next;
        cur.next = clone;
        cur = clone.next;
    }
    // 建立 random 链接
    cur = pHead;
    while (cur != null) {
        RandomListNode clone = cur.next;
        if (cur.random != null)
            clone.random = cur.random.next;
        cur = clone.next;
    }
    // 拆分
    cur = pHead;
    RandomListNode pCloneHead = pHead.next;
    while (cur.next != null) {
        RandomListNode next = cur.next;
        cur.next = next.next;
        cur = next;
    }
    return pCloneHead;
}
改进:
下面的拆分更好理解.

 public RandomListNode Clone(RandomListNode pHead) {
        if (pHead == null) {
            return null;
        }
    	//克隆节点
        RandomListNode cur = pHead;
        while (cur != null) {
            RandomListNode clone = new RandomListNode(cur.label);
            clone.next = cur.next;
            cur.next = clone;
            cur = cur.next.next;
        }
        //克隆随机指针
        cur = pHead;
        while (cur != null) {
            RandomListNode clone = cur.next;
            if (cur.random != null) {
                clone.random = cur.random.next;//为什么判空

            }
            cur = clone.next;
        }
        //拆分
        cur = pHead;
        RandomListNode cloneHead = pHead.next;
        while (cur != null) {
            RandomListNode cloneCur = cur.next;
            cur.next = cloneCur.next;
            cloneCur.next = cloneCur.next == null ? null : cloneCur.next.next;
            //cloneCur=cloneCur.next;
            cur = cur.next;
        }
        return cloneHead;
 }
```

# @@36. 二叉搜索树与双向链表

[NowCoder](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入一棵二叉搜索树，将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点，只能调整树中结点指针的指向。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/05a08f2e-9914-4a77-92ef-aebeaecf4f66.jpg" width="400"/> </div><br>
**解题思路**

```java
private TreeNode pre = null;
private TreeNode head = null;

public TreeNode Convert(TreeNode root) {
    inOrder(root);
    return head;
}

private void inOrder(TreeNode node) {
    if (node == null)
        return;
    inOrder(node.left);
    node.left = pre;
    if (pre != null)
        pre.right = node;
    pre = node;
    if (head == null)
        head = node;
    inOrder(node.right);
}

改进:
pre.right = root;
root.left = pre;
pre = root;

  private TreeNode pre = null;
    private TreeNode head = null;

    public TreeNode Convert(TreeNode pRootOfTree) {
        inOrder(pRootOfTree);
        return head;
    }

    private void inOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        inOrder(root.left);
        if (pre == null) {
            pre = root;
        } else {
            pre.right = root;
            root.left = pre;
            pre = root;
        }

        if (head == null) {
            head = root;
        }
        inOrder(root.right);
    }

改进:非递归写法

    private TreeNode head = null;
    private TreeNode pre = null;

    public TreeNode Convert(TreeNode root) {
        if (root == null) {
            return null;
        }
        TreeNode p = root;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        while (p != null || !stack.isEmpty()) {
            while (p != null) {
                stack.push(p);
                p = p.left;
            }
            p = stack.pop();
            if (pre == null) {
                pre = p;
                head = p;
            } else {
                pre.right = p;
                p.left = pre;
                pre = p;
            }
            p = p.right;
        }
        return head;
    }
```

# 37. 序列化二叉树

[NowCoder](https://www.nowcoder.com/practice/cf7e25aa97c04cc1a68c8f040e71fb84?tpId=13&tqId=11214&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

请实现两个函数，分别用来序列化和反序列化二叉树。

**解题思路**

```java
private String deserializeStr;

public String Serialize(TreeNode root) {
    if (root == null)
        return "#";
    return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
}

public TreeNode Deserialize(String str) {
    deserializeStr = str;
    return Deserialize();
}

private TreeNode Deserialize() {
    if (deserializeStr.length() == 0)
        return null;
    int index = deserializeStr.indexOf(" ");
    String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index);
    deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1);
    if (node.equals("#"))
        return null;
    int val = Integer.valueOf(node);
    TreeNode t = new TreeNode(val);
    t.left = Deserialize();
    t.right = Deserialize();
    return t;
}

改进:

string 转化int    Integer.valueOf(string)
string相等:   string.equals(string)
string长度   string.length()

    String Serialize(TreeNode root) {
        if (root == null) {
            return "#";
        }
        return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);

    }

    private String data;
    TreeNode Deserialize(String str) {
        data = str;
        return Dstr();
    }
    private TreeNode Dstr() {
        if (data.length() == 0) {
            return null;
        }
        int index = data.indexOf(" ");
        String node = index == -1 ? data : data.substring(0, index);
        data = index == -1 ? "" : data.substring(index + 1);
        if (node.equals("#")) {
            return null;
        }
        TreeNode t = new TreeNode(Integer.valueOf(node));
        t.left = Dstr();
        t.right = Dstr();
        return t;
    }
```

# 38. 字符串的排列

[NowCoder](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入一个字符串，按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc，则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。

**解题思路**

```java
    private StringBuilder sb ;
    private boolean[] hasUsed;
    private char[]   chars;
    private Set<String> retSet;
    private ArrayList<String>  ret;
    public ArrayList<String> Permutation(String str) {
       if(str==null||str.length()==0){
           return new ArrayList<String>();
       }

        sb = new StringBuilder();
        int length = str.length();
        hasUsed =new  boolean[length];
        chars = str.toCharArray();
        retSet =new HashSet<String>();

        backtracing();
        ret = new ArrayList<String>(retSet);
        Collections.sort(ret);
        return ret;
    }

    private void backtracing(){
        if(sb.length()==chars.length){
            retSet.add(sb.toString());
            return ;
        }
        for(int i= 0;i<chars.length;i++){
            if(hasUsed[i]){
                continue;
            }
            hasUsed[i]=true;
            sb.append(chars[i]);
            backtracing();
            hasUsed[i]=false;
            sb.deleteCharAt(sb.length()-1);
        }
    }

改进

    StringBuilder s :
	s.append(char c) 		s.deleteCharAt(s.length()-1)
    导入包:                  import java.util.*;
	化为字符串                s.toString()
    字符串化成Array:          str.toCharArray()

    排序函数
    Arrays.sort(array);
	Collections.sort( arraylist);
	new ArrayList(HashSet<String> ret);

```

# 39. 数组中出现次数超过一半的数字

[NowCoder](https://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163?tpId=13&tqId=11181&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**解题思路**

多数投票问题，可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题，使得时间复杂度为 O(N)。

使用 cnt 来统计一个元素出现的次数，当遍历到的元素和统计元素相等时，令 cnt++，否则令 cnt--。如果前面查找了 i 个元素，且 cnt == 0，说明前 i 个元素没有 majority，或者有 majority，但是出现的次数少于 i / 2 ，因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中，majority 的数目依然多于 (n - i) / 2，因此继续查找就能找出 majority。

```java
public int MoreThanHalfNum_Solution(int[] nums) {
    int majority = nums[0];
    for (int i = 1, cnt = 1; i < nums.length; i++) {
        cnt = nums[i] == majority ? cnt + 1 : cnt - 1;
        if (cnt == 0) {
            majority = nums[i];
            cnt = 1;
        }
    }
    int cnt = 0;
    for (int val : nums)
        if (val == majority)
            cnt++;
    return cnt > nums.length / 2 ? majority : 0;
}

改进: 上面的思路值得学习.

 public int MoreThanHalfNum_Solution(int[] array) {
        int maxCount = 0;
        int maxKey = 0;
        HashMap<Integer, Integer> countMap = new HashMap<Integer, Integer>();
        for (int i = 0; i < array.length; i++) {
            int count = 0;
            if (countMap.get(array[i]) != null) {
                count = countMap.get(array[i]);
            }
            count++;
            countMap.put(array[i], count);
            if (count > maxCount) {
                maxCount = count;
                maxKey = array[i];
            }

        }
        if (maxCount * 2 > array.length) {
            return maxKey;
        }
        return 0;
    }
```

# @@@40. 最小的 K 个数

@快排,排出第k位的数,左边的k个数就是要求的.

一些边界的处理总结:

null  length  k>length  目标结果不存在   等等.

[NowCoder](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**解题思路**

### 快速选择

- 复杂度：O(N) + O(1)
- 只有当允许修改数组元素时才可以使用

快速排序的 partition() 方法，会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j]，且 a[j+1..h] 大于等于 a[j]，此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素，这种找第 K 个元素的算法称为快速选择算法。

```java
改进:
我的写法更好理解
ArrayList<Integer> ret = new ArrayList<Integer>();

    public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
        if (k <= 0 || k > input.length) {
            return ret;
        }
        findK(input, k - 1);
        for (int i = 0; i < k; i++) {
            ret.add(input[i]);
        }
        return ret;
    }

    private void findK(int[] input, int k) {
        int low = 0, high = input.length - 1;
        while (low < high) {
            int cutIndex = partition(input, low, high);
            if (cutIndex == k) {
                break;
            }
            if (cutIndex < k) {
                low = cutIndex + 1;
            }
            if (cutIndex > k) {
                high = cutIndex - 1;
            }
        }
    }

    private int partition(int[] input, int low, int high) {
        int left = low, right = high;
        int temp = input[low];
        while (left < right) {
            while (left < right && input[right] >= temp)
                right--;
            input[left] = input[right];
            while (left < right && input[left] <= temp)
                left++;
            input[right] = input[left];
        }
        input[left] = temp;
        return left;
    }
```

### 大小为 K 的最小堆

- 复杂度：O(NlogK) + O(K)
- 特别适合处理海量数据

应该使用大顶堆来维护最小堆，而不能直接创建一个小顶堆并设置一个大小，企图让小顶堆中的元素都是最小元素。因为小顶堆的顶是最小的元素,下面的元素都比顶要大.维护的堆将不再是最小值堆.

维护一个大小为 K 的最小堆过程如下：在添加一个元素之后，如果大顶堆的大小大于 K，那么需要将大顶堆的堆顶元素去除。

```java
public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
    if (k > nums.length || k <= 0)
        return new ArrayList<>();
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
    for (int num : nums) {
        maxHeap.add(num);
        if (maxHeap.size() > k)
            maxHeap.poll();
    }
    return new ArrayList<>(maxHeap);
}

改进:
上面的写法更简洁.

  public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
        if (k <= 0 || k > input.length) {
            return new ArrayList<Integer>();
        }
        Comparator<Integer> c = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        };
        PriorityQueue<Integer> heap = new PriorityQueue<Integer>(k, c);
        for (int temp : input) {
            if (heap.size() < k) {
                heap.add(temp);
            } else {
                int maxNum = heap.peek();
                if (maxNum > temp) {
                    heap.poll();
                    heap.add(temp);
                }
            }
        }
        ArrayList<Integer> list = new ArrayList<Integer>(heap);
        return list;
    }
```

# 快速排序

```java
    public int[] quickSort(int[] nums, int low, int high) {
        if (low < high) {
            int cutIndex = partition(nums, low, high);
            quickSort(nums, low, cutIndex - 1);
            quickSort(nums, cutIndex + 1, high);
        }
        return nums;
    }

    private int partition(int[] nums, int low, int high) {
        int left = low, right = high;
        int temp = nums[left];
        while (left < right) {
            while (left < right && nums[right] >= temp) {
                right--;
            }
            nums[left] = nums[right];
            while (left < right && nums[left] <= temp) {
                left++;
            }
            nums[right] = nums[left];
        }
        nums[left] = temp;
        return left;
    }

```

#  41.1 数据流中的中位数

[NowCoder](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

如何得到一个数据流中的中位数？如果从数据流中读出奇数个数值，那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值，那么中位数就是所有数值排序之后中间两个数的平均值。

**解题思路**

```java
/* 大顶堆，存储左半边元素 */
private PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1);
/* 小顶堆，存储右半边元素，并且右半边元素都大于左半边 */
private PriorityQueue<Integer> right = new PriorityQueue<>();
/* 当前数据流读入的元素个数 */
private int N = 0;

public void Insert(Integer val) {
    /* 插入要保证两个堆存于平衡状态 */
    if (N % 2 == 0) {
        /* N 为偶数的情况下插入到右半边。
         * 因为右半边元素都要大于左半边，但是新插入的元素不一定比左半边元素来的大，
         * 因此需要先将元素插入左半边，然后利用左半边为大顶堆的特点，取出堆顶元素即为最大元素，此时插入右半边 */
        left.add(val);
        right.add(left.poll());
    } else {
        right.add(val);
        left.add(right.poll());
    }
    N++;
}

public Double GetMedian() {
    if (N % 2 == 0)
        return (left.peek() + right.peek()) / 2.0;
    else
        return (double) right.peek();
}
```

# 41.2 字符流中第一个不重复的字符

[NowCoder](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如，当从字符流中只读出前两个字符 "go" 时，第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时，第一个只出现一次的字符是 "l"。

**解题思路**

```java
private int[] cnts = new int[256];
private Queue<Character> queue = new LinkedList<>();

public void Insert(char ch) {
    cnts[ch]++;
    queue.add(ch);
    while (!queue.isEmpty() && cnts[queue.peek()] > 1)
        queue.poll();
}

public char FirstAppearingOnce() {
    return queue.isEmpty() ? '#' : queue.peek();
}

改进 :
有队列来记录最早的符合条件的node
queue用linkedList来实现.   最早的节点在头上,最晚的节点在尾巴,poll的时候从头上取.
用数组的下标来作为key.cnts[ch]++;
```

# 42. 连续子数组的最大和

[NowCoder](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

{6, -3, -2, 7, -15, 1, 2, 2}，连续子数组的最大和为 8（从第 0 个开始，到第 3 个为止）。

注意: 并不是一定要从0开始,可以是数组中间的一段连续子数组.

**解题思路**

```java
public int FindGreatestSumOfSubArray(int[] nums) {
    if (nums == null || nums.length == 0)
        return 0;
    int greatestSum = Integer.MIN_VALUE;
    int sum = 0;
    for (int val : nums) {
        sum = sum <= 0 ? val : sum + val;
        greatestSum = Math.max(greatestSum, sum);
    }
    return greatestSum;
}

改进:
Integer.MIN_VALUE;
Math.max(,);
```

# @@42.2二维矩阵中的最大子矩阵

题目描述:

链接：https://www.nowcoder.com/questionTerminal/a5a0b05f0505406ca837a3a76a5419b3?f=discussion
来源：牛客网

已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵，你的任务是找到最大的非空(大小至少是1 * 1)子矩阵。 比如，如下4 * 4的矩阵 0 -2 -7 0 9 2 -6 2 -4 1 -4 1 -1 8 0 -2 的最大子矩阵是 9 2 -4 1 -1 8 这个子矩阵的大小是15。

**输入描述:**

```
输入是一个N * N的矩阵。输入的第一行给出N (0 < N <= 100)。
再后面的若干行中，依次（首先从左到右给出第一行的N个整数，再从左到右给出第二行的N个整数……）给出矩阵中的N2个整数，整数之间由空白字符分隔（空格或者空行）
已知矩阵中整数的范围都在[-127, 127]。
```

**输出描述:**

```
测试数据可能有多组，对于每组测试数据，输出最大子矩阵的大小。
```

```java
改进:
思路:
i从0 到 n
j从i 到 n
i到j行累加成一行,在一行上求最大连续子序列.最终的max,就是 第i到第j行的组合下,某几列的连续子序列.

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[][] array = new int[n][n];

        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                array[i][j] = sc.nextInt();

        System.out.println(getMaxSub(array, n));
    }

    public static int getMaxSub(int[][] matrix, int n) {
        int max = Integer.MIN_VALUE;
        int[] cumulation = new int[n];
        for (int i = 0; i < n; i++) {
            for (int j = i; j < n; j++) {
                getCumulation(matrix, cumulation, i, j, n);
                int temp = dpFunc(cumulation);
                max = Math.max(temp, max);
            }
        }
        return max;
    }

    private static int dpFunc(int[] cumulation) {
        int len = cumulation.length;
        int[] dp = new int[len];

        dp[0] = cumulation[0];
        int max = dp[0];
        for (int i = 1; i < len; i++) {
            dp[i] = Math.max(dp[i - 1] + cumulation[i], cumulation[i]);
            max = Math.max(dp[i], max);
        }
        return max;
    }

    private static void getCumulation(int[][] matrix, int[] cumulation, int begin, int end, int n) {
        for (int i = 0; i < n; i++)
            cumulation[i] = 0;
        for (int i = begin; i <= end; i++) {
            for (int j = 0; j < n; j++) {
                cumulation[j] += matrix[i][j];
            }
        }
    }
}
```

# @@@@43. 从 1 到 n 整数中 1 出现的次数

@base也要使用long

除了sum,其他的数字都要用long,尤其是base, 因为base有一个base*10的操作,会溢出,

long和int之间可以做加减乘除,不必类型转换.

[NowCoder](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&tqId=11184&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**解题思路**

```java
public int NumberOf1Between1AndN_Solution(int num) {

//            以base=10的循环为例,解释上面的代码:
//            base=10,假设num=317,
//            oneInBase: 计算十位上是1,等时候出现的数字个数
//                    num除以100,得出来的是3,
//                    也就是3个100,每个100里面,十位上是1都有10次出现的机会.
//                    所以(num / base * 10) * base的末尾再乘以 base,也就是乘以10.
//                    得出317的oneInI等于30
//            oneInRemain是300~317之间出现1的次数.
//            分三种情况.如果17大于19(20~99),如果17小于10(0~9),如果17在10~19之间(10~19)
//            对应的1的个数分别是10个 (10~19)     0个      17-10+1个(10,11,12,13...17)
//			所以最后317在十位上的1的个数为: 317/(10*10)*10+(17-10+1) = 38

        if (num <= 0)
            return 0;
        int countSum = 0;
        long oneInRemain = 0;
        long oneInBase = 0;
        //base=1时,计算的是个位里面1的个数
        //base=10时,计算的是十位里1的个数
        for (long base = 1; base <= num; base *= 10) {
            //计算OneInI
            oneInBase = (num / (base * 10)) * base;

            //计算OnInRemain
            long remain = num % (base * 10);
            if (remain > base * 2 - 1)
                oneInRemain = base;
            else if (remain < base)
                oneInRemain = 0;
            else if(remain>=base&&remain<=base*2-1) //>=10&&<=19
                oneInRemain = remain - base + 1;
            //计算总的countSum
            countSum += oneInBase + oneInRemain;
        }
        return countSum;
    }
```

> [Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython)

# @44. 数字序列中的某一位数字

@ 0~9 1位数  10~99 2位数

**题目描述**

数字以 0123456789101112131415... 的格式序列化到一个字符串中，求这个字符串的第 index 位。

解题思路

```java
public int getDigitAtIndex(int index) {
    if (index < 0)
        return -1;
    int place = 1;  // 1 表示个位，2 表示 十位...
    while (true) {
        int amount = getAmountOfPlace(place);
        int totalAmount = amount * place;
        if (index < totalAmount)
            return getDigitAtIndex(index, place);
        index -= totalAmount;
        place++;
    }
}

/**
 * place 位数的数字组成的字符串长度
 * 10, 90, 900, ...
 */
private int getAmountOfPlace(int place) {
    if (place == 1)
        return 10;
    return (int) Math.pow(10, place - 1) * 9;
}

/**
 * place 位数的起始数字
 * 0, 10, 100, ...
 */
private int getBeginNumberOfPlace(int place) {
    if (place == 1)
        return 0;
    return (int) Math.pow(10, place - 1);
}

/**
 * 在 place 位数组成的字符串中，第 index 个数
 */
private int getDigitAtIndex(int index, int place) {
    int beginNumber = getBeginNumberOfPlace(place);
    int shiftNumber = index / place;
    String number = (beginNumber + shiftNumber) + "";
    int count = index % place;
    return number.charAt(count) - '0';
}
```

# @45. 把数组排成最小的数

[NowCoder](https://www.nowcoder.com/practice/8fecd3f8ba334add803bf2a06af1b993?tpId=13&tqId=11185&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入一个正整数数组，把数组里所有数字拼接起来排成一个数，打印能拼接出的所有数字中最小的一个。例如输入数组 {3，32，321}，则打印出这三个数字能排成的最小数字为 321323。

解题思路

可以看成是一个排序问题，在比较两个字符串 S1 和 S2 的大小时，应该比较的是 S1+S2 和 S2+S1 的大小，如果 S1+S2 < S2+S1，那么应该把 S1 排在前面，否则应该把 S2 排在前面。

```java
    public String PrintMinNumber(int[] numbers) {
        if (numbers.length == 0 || numbers == null) {
            return "";
        }
        String[] numStr = new String[numbers.length];
        for (int i = 0; i < numbers.length; i++) {
            numStr[i] = numbers[i] + "";
        }
        Comparator<String> c = new Comparator<String>() {
            public int compare(String s1, String s2) {
                return (s1 + s2).compareTo(s2 + s1);
            }
        };
        Arrays.sort(numStr, c);
        String ret = "";
        for (String temp : numStr) {
            ret += temp;
        }
        return ret;
    }

改进:
String 的比较 str.compareTo(s2)
Array.sort(nums,comparator);
此题的核心在于s1 和 s2的 大小是由 s1+s2 ,s2+s1 的大小来决定的.
```

# @46. 把数字翻译成字符串

[Leetcode](https://leetcode.com/problems/decode-ways/description/)

**题目描述**

给定一个数字，按照如下规则翻译成字符串：1 翻译成“a”，2 翻译成“b”... 26 翻译成“z”。一个数字有多种翻译可能，例如 12258 一共有 5 种，分别是 abbeh，lbeh，aveh，abyh，lyh。实现一个函数，用来计算一个数字有多少种不同的翻译方法。

**解题思路**

```java
public int numDecodings(String s) {
    if (s == null || s.length() == 0)
        return 0;
    int n = s.length();
    int[] dp = new int[n + 1];
    dp[0] = 1;
    dp[1] = s.charAt(0) == '0' ? 0 : 1;
    for (int i = 2; i <= n; i++) {
        int one = Integer.valueOf(s.substring(i - 1, i));
        if (one != 0)
            dp[i] += dp[i - 1];
        if (s.charAt(i - 2) == '0')
            continue;
        int two = Integer.valueOf(s.substring(i - 2, i));
        if (two <= 26)
            dp[i] += dp[i - 2];
    }
    return dp[n];
}

改进: 难懂的代码,把数字带进去,仔细读懂每一句.
    dp[0]是边界条件,dp[1]是第一个字符的组合数,dp[2]是第二个字符的组合数
    substring,charAt的下标是从0开始的.
    Integer.valueOf(s.substring(1,2))  根据字符串代表的数字返回int        '0' 返回 0
    Integer.valueOf(s.charAt(1))       根据Character对应的int值返回int: '0' 返回 48

```

# 47. 礼物的最大价值

[NowCoder](https://www.nowcoder.com/questionTerminal/72a99e28381a407991f2c96d8cb238ab)

**题目描述**

在一个 m\*n 的棋盘的每一个格都放有一个礼物，每个礼物都有一定价值（大于 0）。从左上角开始拿礼物，每次向右或向下移动一格，直到右下角结束。给定一个棋盘，求拿到礼物的最大价值。例如，对于如下棋盘

```
1    10   3    8
12   2    9    6
5    7    4    11
3    7    16   5
```

礼物的最大价值为 1+12+5+7+7+16+5=53。

**解题思路**

应该用动态规划求解，而不是深度优先搜索，深度优先搜索过于复杂，不是最优解。

```java
public int getMost(int[][] values) {
    if (values == null || values.length == 0 || values[0].length == 0)
        return 0;
    int n = values[0].length;
    int[] dp = new int[n];
    for (int[] value : values) {
        dp[0] += value[0];
        for (int i = 1; i < n; i++)
            dp[i] = Math.max(dp[i], dp[i - 1]) + value[i];
    }
    return dp[n - 1];
}

改进: 二维数组的长和宽不一定相等,要分开考虑
    public int getMost(int[][] board) {
        // write code here
        int row = board.length;
        int col = board[0].length;
        int[][] dp = new int[row][col];
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (i == 0 && j == 0) {
                    dp[0][0] = board[0][0];
                } else if (i == 0) {
                    dp[i][j] = board[i][j] + dp[i][j - 1];
                } else if (j == 0) {
                    dp[i][j] = board[i][j] + dp[i - 1][j];
                } else {
                    dp[i][j] = board[i][j] + Math.max(dp[i - 1][j], dp[i][j - 1]);
                }

            }
        }
        return dp[row - 1][col - 1];
    }
```

# 48. 最长不含重复字符的子字符串

**题目描述**

输入一个字符串（只包含 a\~z 的字符），求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr，最长不含重复字符的子字符串为 acfr，长度为 4。

**解题思路**

```java
public int longestSubStringWithoutDuplication(String str) {
    int curLen = 0;
    int maxLen = 0;
    int[] preIndexs = new int[26];
    Arrays.fill(preIndexs, -1);
    for (int curI = 0; curI < str.length(); curI++) {
        int c = str.charAt(curI) - 'a';
        int preI = preIndexs[c];
        if (preI == -1 || curI - preI > curLen) {
            curLen++;
        } else {
            maxLen = Math.max(maxLen, curLen);
            curLen = curI - preI;
        }
        preIndexs[c] = curI;
    }
    maxLen = Math.max(maxLen, curLen);
    return maxLen;
}
改进:
int m = 5 + 4 > 3 ? 1 : 6;  会被当成 9>3?1:6来做,所以m=1.
思路:
dp[n]只由dp[n-1]决定,
如果dp[n-1]所代表的子串内,没有dp[n-1]的重复则dp[n]=dp[n-1]+1;
如果dp[n-1]所代表的的子串内,从后往前,在k处发现了与dp[n]对应的重复,则dp[n]=n-k;
 public int func(String s) {
        if (s == null || s.length() == 0) {
            return 0;
        }
        int len = s.length();
        int[] dp = new int[len];
        dp[0] = 1;
        int maxLen = 0;
        for (int i = 1; i < len; i++) {
            int preLongest = dp[i - 1];
            int startPre = i - preLongest;
            int repeatIndex = -1;
            for (int j = i - 1; j >= startPre; j--) {
                if (s.charAt(j) == s.charAt(i)) {
                    repeatIndex = j;
                    break;
                }
            }
            if (repeatIndex < 0) {
                dp[i] = dp[i - 1] + 1;
                maxLen = Math.max(dp[i], maxLen);
            } else {
                dp[i] = i - repeatIndex;
                maxLen = Math.max(dp[i], maxLen);
            }
        }
        return maxLen;
    }
```

# @49. 丑数

@思路不难,难在如何找下一个丑数,因为`2*2*2*2  > 3*3`

@注意三个if之间并非互斥的  123456  下一个数  因为 nums[idx3] = 2;所以  2*3 = 6,但6已经存在,所以idx3应该++.

[NowCoder](https://www.nowcoder.com/practice/6aa9e04fc3794f68acf8778237ba065b?tpId=13&tqId=11186&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

把只包含因子 2、3 和 5 的数称作丑数（Ugly Number）。例如 6、8 都是丑数，但 14 不是，因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。

解题思路:

链接：https://www.nowcoder.com/questionTerminal/6aa9e04fc3794f68acf8778237ba065b?f=discussion
来源：牛客网

首先从丑数的定义我们知道，一个丑数的因子只有2,3,5，那么丑数p = 2 ^ x * 3 ^ y * 5 ^ z，换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到，那么我们从1开始乘以2,3,5，就得到2,3,5三个丑数，在从这三个丑数出发乘以2,3,5就得到4，6,10,6，

**解题思路**

```java
public int GetUglyNumber_Solution(int N) {
    if (N <= 6)
        return N;
    int i2 = 0, i3 = 0, i5 = 0;
    int[] dp = new int[N];
    dp[0] = 1;
    for (int i = 1; i < N; i++) {
        int next2 = dp[i2] * 2, next3 = dp[i3] * 3, next5 = dp[i5] * 5;
        dp[i] = Math.min(next2, Math.min(next3, next5));
        if (dp[i] == next2)
            i2++;
        if (dp[i] == next3)
            i3++;
        if (dp[i] == next5)
            i5++;
    }
    return dp[N - 1];
}

改进:
next2,next3,next4 是候选数,候选数的计算方式很重要.
    如果当前选择的是next2,则下一次的next2是 ret.get(idx2)*2;
	最重要的是 idx2 idx3 idx5使用的是独立下标!!!
public int GetUglyNumber_Solution(int n) {
        if (n == 0) {
            return 0;
        }
        ArrayList<Integer> ret = new ArrayList<Integer>();
        int idx2 = 0;
        int idx3 = 0;
        int idx5 = 0;
        ret.add(1);

        for (int i = 2; i <= n; i++) {
            int next2 = ret.get(idx2) * 2;
            int next3 = ret.get(idx3) * 3;
            int next5 = ret.get(idx5) * 5;
            int next = Math.min(next2, Math.min(next3, next5));
            ret.add(next);
            if (next == next2) {
                idx2++;
            }
            if (next == next3) {
                idx3++;
            }
            if (next == next5) {
                idx5++;
            }
        }
        return ret.get(n - 1);
    }
```

# 50. 第一个只出现一次的字符位置

[NowCoder](https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c?tpId=13&tqId=11187&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

在一个字符串中找到第一个只出现一次的字符，并返回它的位置。

```
Input: abacc
Output: b
```

**解题思路**

最直观的解法是使用 HashMap 对出现次数进行统计，但是考虑到要统计的字符范围有限，因此可以使用整型数组代替 HashMap，从而将空间复杂度由 O(N) 降低为 O(1)。

```java
public int FirstNotRepeatingChar(String str) {
    int[] cnts = new int[256];
    for (int i = 0; i < str.length(); i++)
        cnts[str.charAt(i)]++;
    for (int i = 0; i < str.length(); i++)
        if (cnts[str.charAt(i)] == 1)
            return i;
    return -1;
}
```

以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符，那么需要统计的次数信息只有 0,1,更大，使用两个比特位就能存储这些信息。

```java
public int FirstNotRepeatingChar2(String str) {
    BitSet bs1 = new BitSet(256);//是否存在
    BitSet bs2 = new BitSet(256);//是否多次
    for (char c : str.toCharArray()) {
        if (!bs1.get(c) && !bs2.get(c))
            bs1.set(c);     // 0 0 -> 1 0   不存在,        改成存在,但不是多次
        else if (bs1.get(c) && !bs2.get(c))
            bs2.set(c);     // 1 0 -> 1 1 存在,但不是多次  改成存在,且多次
    }
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (bs1.get(c) && !bs2.get(c))  // 0 1
            return i;
    }
    return -1;
}

改进: 字符有256个
    int[] flags = new int[256];//字符
	int[] flags = new int[26];//字母
```

# @@51. 数组中的逆序对

@//如果左边leftIdx的值 大于右边的有序序列
                //则左边从leftIdx到mid的值,都可以与rightIdx的值组成一个逆序对.

[NowCoder](https://www.nowcoder.com/practice/96bd6684e04a44eb80e6a68efc0ec6c5?tpId=13&tqId=11188&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

在数组中的两个数字，如果前面一个数字大于后面的数字，则这两个数字组成一个逆序对。输入一个数组，求出这个数组中的逆序对的总数。

**解题思路**

```java
改进:

归并排序学习

int	-2147483648～2147483647  10位,最高位为2
long	Long.MAX_VALUE=9223372036854775807  19位,最高位为9

一般编程题模1000000007(十位,质数)1000000007 是最小的十位质数。模1000000007，可以保证值永远在int的范围内。

模1000000007的原因是:
1. 1000000007是一个质数
2. int32位的最大值为2147483647，所以对于int32位来说1000000007足够大
3. int64位的最大值为2^63-1，对于1000000007来说它的平方不会在int64中溢出 所以在大数相乘的时候，因为(a∗b)%c=((a%c)∗(b%c))%c，所以相乘时两边都对1000000007取模，再保存在int64里面不会溢出

 private long count = 0;
    private int[] tmp;

    public int InversePairs(int[] array) {

        tmp = new int[array.length];
        mergeSort(array, 0, array.length - 1);
        return (int) (count % 1000000007);
    }

    private void mergeSort(int[] array, int low, int high) {
        if (high - low < 1) {
            //改进: high-low<1
            return;
        }
//        int mid = (low + high) / 2;
        int mid = low + (high - low) / 2;
        //改进: low+(high-low)/2
        mergeSort(array, low, mid);
        mergeSort(array, mid + 1, high);
        merge(array, low, mid, high);
    }

    private void merge(int[] array, int low, int mid, int high) {
        int leftIdx = low, rightIdx = mid + 1, tempIdx = low;
        while (leftIdx <= mid || rightIdx <= high) {
            if (leftIdx > mid) {
                //123  456
                //456  123
                tmp[tempIdx] = array[rightIdx++];
            } else if (rightIdx > high) {
                tmp[tempIdx] = array[leftIdx++];
            } else if (array[leftIdx] <= array[rightIdx]) {
                tmp[tempIdx] = array[leftIdx++];
            } else if(array[leftIdx]>array[rightIdx]{
                //如果左边leftIdx的值 大于右边的有序序列
                //则左边从leftIdx到mid的值,都可以与rightIdx的值组成一个逆序对.
                tmp[tempIdx] = array[rightIdx++];
                this.count += mid - leftIdx + 1;
            }
            tempIdx++;
        }
        for (tempIdx = low; tempIdx <= high; tempIdx++) {
            array[tempIdx] = tmp[tempIdx];
        }
    }
```

# @@52. 两个链表的第一个公共结点

[NowCoder](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&tqId=11189&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5f1cb999-cb9a-4f6c-a0af-d90377295ab8.png" width="500"/> </div><br>
**解题思路**

设 A 的长度为 a + c，B 的长度为 b + c，其中 c 为尾部公共部分长度，可知 a + c + b = b + c + a。

当访问链表 A 的指针访问到链表尾部时，令它从链表 B 的头部重新开始访问链表 B；同样地，当访问链表 B 的指针访问到链表尾部时，令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。

```java
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        ListNode l1 = pHead1;
        ListNode l2 = pHead2;
        while (l1 != l2) {

            if (l1 == null) {
                l1 = pHead2;
            } else {
                l1 = l1.next;
            }

            if (l2 == null) {
                l2 = pHead1;
            } else {
                l2 = l2.next;
            }

        }
        return l1;
    }

改进:
下面的方法好理解求size1 size2   和 deta差.
    让长的先走deta差.
 public ListNode FindFirstCommonNode(ListNode p1, ListNode p2) {
        int size1 = 0;
        int size2 = 0;
        ListNode pHead1 = p1;
        ListNode pHead2 = p2;
        while (pHead1 != null) {
            pHead1 = pHead1.next;
            size1++;
        }
        while (pHead2 != null) {
            pHead2 = pHead2.next;
            size2++;
        }
        int diff = Math.abs(size1 - size2);

        if (size1 > size2) {
            pHead1 = p1;
            pHead2 = p2;
        } else {
            pHead1 = p2;
            pHead2 = p1;
        }
        for (int i = 0; i < diff; i++) {
            pHead1 = pHead1.next;
        }
        while (pHead1 != null) {
            if (pHead1.val == pHead2.val) {
                return pHead1;
            }
            pHead1 = pHead1.next;
            pHead2 = pHead2.next;
        }
        return pHead1;
    }
```

# 53. 数字在排序数组中出现的次数

[NowCoder](https://www.nowcoder.com/practice/70610bf967994b22bb1c26f9ae901fa2?tpId=13&tqId=11190&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

```html
Input:
nums = 1, 2, 3, 3, 3, 3, 4, 6
K = 3

Output:
4
```

**解题思路**

```java
public int GetNumberOfK(int[] nums, int K) {
    int first = binarySearch(nums, K);
    int last = binarySearch(nums, K + 1);
    return (first == nums.length || nums[first] != K) ? 0 : last - first;
}

private int binarySearch(int[] nums, int K) {
    int l = 0, h = nums.length;
    while (l < h) {
        int m = l + (h - l) / 2;
        if (nums[m] >= K)
            h = m;
        else
            l = m + 1;
    }
    return l;
}

改进: upper, lower的写法
    getupper返回的是high ,因为在最后阶段,low和high和mid都会落在一个大于k的值上,只有high会再经历减一,回到k
    最后一次出现的地方.

    getlower返回的是low,理由同上.

  public int GetNumberOfK(int[] array, int k) {
        int end = getUpper(array, k);
        int start = getLower(array, k);
        return end - start + 1;
    }

    private int getUpper(int[] array, int k) {
        int low = 0, high = array.length - 1;
        int mid = (low + high) / 2;
        while (low <= high) {
            if (array[mid] > k) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
            mid = (low + high) / 2;
        }
        return high;//mid不行
    }

    private int getLower(int[] array, int k) {
        int low = 0, high = array.length - 1;
        int mid = (low + high) / 2;
        while (low <= high) {
            if (array[mid] >= k) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
            mid = (low + high) / 2;
        }
        return low;//mid不行
    }

改进: k-0.5 与 k+0.5

//因为data中都是整数，所以可以稍微变一下，不是搜索k的两个位置，而是搜索k-0.5和k+0.5
//这两个数应该插入的位置，然后相减即可。
class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) {
        return biSearch(data, k+0.5) - biSearch(data, k-0.5) ;
    }
private:
    int biSearch(const vector<int> & data, double num){
        int s = 0, e = data.size()-1;
        while(s <= e){
            int mid = (e - s)/2 + s;
            if(data[mid] < num)
                s = mid + 1;
            else if(data[mid] > num)
                e = mid - 1;
        }
        return s;
    }
};

```

# 54. 二叉查找树的第 K 个结点

[NowCoder](https://www.nowcoder.com/practice/ef068f602dde4d28aab2b210e859150a?tpId=13&tqId=11215&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**解题思路**

利用二叉查找树中序遍历有序的特点。

```java
  改进:
如果是节点顺序的cont,count++写在节点处.
    如果是节点层次遍历,count++和count--把 midOrder(left),midOrder(right)包起来 ;其实相当于回溯.

private int count = 0;
    private TreeNode val = null;

    TreeNode KthNode(TreeNode pRoot, int k) {
        if (pRoot == null) {
            return null;
        }
        midOrder(pRoot, k);
        return val;
    }

    private void midOrder(TreeNode root, int k) {
        if (root == null) {
            return;
        }

        midOrder(root.left, k);
        count++;
        if (count == k) {
            val = root;
            return;
        }
        midOrder(root.right, k);
    }
```

# 55.1 二叉树的深度

[NowCoder](https://www.nowcoder.com/practice/435fb86331474282a3499955f0a41e8b?tpId=13&tqId=11191&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

从根结点到叶结点依次经过的结点（含根、叶结点）形成树的一条路径，最长路径的长度为树的深度。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ba355101-4a93-4c71-94fb-1da83639727b.jpg" width="350px"/> </div><br>
**解题思路**

```java
public int TreeDepth(TreeNode root) {
    return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right));
}
改进:
private int max = 0;
    private int depth = 0;

    public int TreeDepth(TreeNode root) {
        midOrder(root);
        return max;
    }

    public void midOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        depth++;
        midOrder(root.left);

        if (depth > max) {
            max = depth;
        }
        midOrder(root.right);
        depth--;
    }
```

# 55.2 平衡二叉树

[NowCoder](https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=13&tqId=11192&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

平衡二叉树左右子树高度差不超过 1。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/af1d1166-63af-47b6-9aa3-2bf2bd37bd03.jpg" width="250px"/> </div><br>
**解题思路**

```java
private boolean isBalance = true;

    public boolean IsBalanced_Solution(TreeNode root) {
        if (root == null) {
            return true;
        }
        judgeBalance(root);
        return isBalance;
    }

    private int judgeBalance(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int leftHight = judgeBalance(root.left);
        int rightHight = judgeBalance(root.right);
        if (Math.abs(leftHight - rightHight) > 1) {
            isBalance = false;
        }
        return Math.max(leftHight, rightHight) + 1;
    }
```

# @56. 数组中只出现一次的数字

[NowCoder](https://www.nowcoder.com/practice/e02fdb54d7524710a7d664d082bb7811?tpId=13&tqId=11193&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

一个整型数组里除了两个数字之外，其他的数字都出现了两次，找出这两个数。

**解题思路**

两个不相等的元素在位级表示上必定会有一位存在不同，将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。

```java

链接：https://www.nowcoder.com/questionTerminal/e02fdb54d7524710a7d664d082bb7811?f=discussion
来源：牛客网

首先：位运算中异或的性质：两个相同数字异或=0，一个数和0异或还是它本身。

当只有一个数出现一次时，我们把数组中所有的数，依次异或运算，最后剩下的就是落单的数，因为成对儿出现的都抵消了。

依照这个思路，我们来看两个数（我们假设是AB）出现一次的数组。我们首先还是先异或，剩下的数字肯定是A、B异或的结果，这个结果的二进制中的1，表现的是A和B的不同的位。我们就取第一个1所在的位数，假设是第3位，接着把原数组分成两组，分组标准是第3位是否为1。如此，相同的数肯定在一个组，因为相同数字所有位都相同，而不同的数，肯定不在一组。然后把这两个组按照最开始的思路，依次异或，剩余的两个结果就是这两个只出现一次的数字。

public void FindNumsAppearOnce(int[] array, int num1[], int num2[]) {
        if (array == null || array.length == 0) {
            return;
        }
        if (array.length == 2) {
            num1[0] = array[0];
            num2[0] = array[1];
            return;
        }
        int bitResult = 0;
        for (int i = 0; i < array.length; i++) {
            bitResult = bitResult ^ array[i];
        }
        int index = findFirst1(bitResult);
        for (int i = 0; i < array.length; i++) {
            if (isBit1(array[i], index)) {
                num1[0] ^= array[i];
            } else {
                num2[0] ^= array[i];
            }
        }
        return;

    }

    private int findFirst1(int bitResult) {
        int index = 0;
        while ((bitResult & 1) == 0 && index < 32) {
            bitResult >>= 1;
            index++;
        }
        return index;
    }

    private boolean isBit1(int target, int index) {
        return ((target >> index) & 1) == 1;
    }
```

# 57.1 和为 S 的两个数字

[NowCoder](https://www.nowcoder.com/practice/390da4f7a00f44bea7c2f3d19491311b?tpId=13&tqId=11195&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输入一个递增排序的数组和一个数字 S，在数组中查找两个数，使得他们的和正好是 S。如果有多对数字的和等于 S，输出两个数的乘积最小的。

**解题思路**

使用双指针，一个指针指向元素较小的值，一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历，指向较大元素的指针从尾向头遍历。

- 如果两个指针指向元素的和 sum == target，那么得到要求的结果；
- 如果 sum > target，移动较大的元素，使 sum 变小一些；
- 如果 sum < target，移动较小的元素，使 sum 变大一些。

```java
public ArrayList<Integer> FindNumbersWithSum(int[] array, int sum) {
    int i = 0, j = array.length - 1;
    while (i < j) {
        int cur = array[i] + array[j];
        if (cur == sum)
            return new ArrayList<>(Arrays.asList(array[i], array[j]));
        if (cur < sum)
            i++;
        else
            j--;
    }
    return new ArrayList<>();
}

public ArrayList<Integer> FindNumbersWithSum(int[] array, int target) {
        if (array == null || array.length == 0) {
            return new ArrayList<Integer>();
        }
        int low = 0, high = array.length - 1;
        while (low < high) {
            int sum = array[low] + array[high];
            if (sum == target) {
                ArrayList<Integer> ret = new ArrayList<Integer>();
                ret.add(array[low]);
                ret.add(array[high]);
                return ret;
            } else if (sum > target) {
                high--;
            } else {
                low++;
            }
        }
        return new ArrayList<Integer>();
    }
```

# 57.2 和为 S 的连续正数序列

[NowCoder](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&tqId=11194&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

输出所有和为 S 的连续正数序列。

例如和为 100 的连续序列有：

```
[9, 10, 11, 12, 13, 14, 15, 16]
[18, 19, 20, 21, 22]
```

**解题思路**

```java
 ArrayList<ArrayList<Integer>> ret = new ArrayList<ArrayList<Integer>>();

    public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
        if (sum <= 0) {
            return ret;
        }
        int start = 1, end = 1;
        int addSum = 1;
        while (end < sum) {
            if (addSum < sum) {
                end++;
                addSum += end;
            } else if (addSum > sum) {
                addSum -= start;
                start++;
            } else {
                int temp = start;
                ArrayList<Integer> list = new ArrayList<Integer>();
                while (start <= end) {
                    list.add(start);
                    start++;
                }
                start = temp + 1;
                end = start;
                addSum = end;
                ret.add(list);
            }
        }
        return ret;
    }
```

# @58.1 翻转单词顺序列

[NowCoder](https://www.nowcoder.com/practice/3194a4f4cf814f63919d0790578d51f3?tpId=13&tqId=11197&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

```html
Input:
"I am a student."

Output:
"student. a am I"
```

**解题思路**

题目应该有一个隐含条件，就是不能用额外的空间。虽然 Java 的题目输入参数为 String 类型，需要先创建一个字符数组使得空间复杂度为 O(N)，但是正确的参数类型应该和原书一样，为字符数组，并且只能使用该字符数组的空间。任何使用了额外空间的解法在面试时都会大打折扣，包括递归解法。

正确的解法应该是和书上一样，先旋转每个单词，再旋转整个字符串。

```java
改进:
String  和 array之间的转换
    str.toCharArray();
	new String(chars[]);

public String ReverseSentence(String str) {
        if (str == null || str.length() == 0) {
            return "";
        }
        int len = str.length();
        char[] chars = str.toCharArray();
        for (int fast = 0, slow = 0; fast < len; fast++) {
            if (chars[fast] == ' ') {
                reverse(chars, slow, fast - 1);
                slow = fast + 1;
            } else if (fast == len - 1) {
                reverse(chars, slow, fast);
                slow = fast + 1;
            }

        }
        reverse(chars, 0, len - 1);
        return new String(chars);

    }

    private void reverse(char[] chars, int slow, int fast) {
        while (slow < fast) {
            char temp = chars[slow];
            chars[slow] = chars[fast];
            chars[fast] = temp;
            slow++;
            fast--;
        }
        return;
    }
```

# @58.2 左旋转字符串

[NowCoder](https://www.nowcoder.com/practice/12d959b108cb42b1ab72cef4d36af5ec?tpId=13&tqId=11196&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

```html
Input:
S="abcXYZdef"
K=3

Output:
"XYZdefabc"
```

**解题思路**

先将 "abc" 和 "XYZdef" 分别翻转，得到 "cbafedZYX"，然后再把整个字符串翻转得到 "XYZdefabc"。

```java
 public String LeftRotateString(String str, int n) {
        if (str == null || str.length() == 0) {
            return "";
        }
        int len = str.length();
        n = n % len;
        char[] chars = str.toCharArray();
        reverse(chars, 0, n - 1);
        reverse(chars, n, len - 1);
        reverse(chars, 0, len - 1);
        return new String(chars);

    }

    private void reverse(char[] chars, int start, int end) {
        while (start < end) {
            char temp = chars[start];
            chars[start] = chars[end];
            chars[end] = temp;
            start++;
            end--;
        }
    }

```

# @@@@59. 滑动窗口的最大值

[NowCoder](https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&tqId=11217&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

给定一个数组和滑动窗口的大小，找出所有滑动窗口里数值的最大值。

例如，如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3，那么一共存在 6 个滑动窗口，他们的最大值分别为 {4, 4, 6, 6, 6, 5}。

**解题思路**

```java
@@@@@@更好地思路

 public ArrayList<Integer> maxInWindows(int[] nums, int size) {
        PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10, (o2, o1) -> (o1 - o2));
        ArrayList<Integer> ret = new ArrayList<Integer>();
        if (size <= 0 || size > nums.length) {
            return ret;
        }
        for (int i = 0; i < size; i++) {
            queue.add(nums[i]);
        }
        ret.add(queue.peek());
        for (int i = 0, j = i + size; j < nums.length; j++, i++) {
            queue.remove(nums[i]);
            queue.add(nums[j]);
            ret.add(queue.peek());
        }
        return ret;
    }

改进
    维护一个双端队列,队头放窗口内的最大值,当有num[i]>deque.getLast()的时候,删除deque中所有比num[i]小的节点的index,让num[i]的i成为队或者原来的deque.getFirst()保持队头.

public ArrayList<Integer> maxInWindows(int[] num, int size) {
        ArrayList<Integer> ret = new ArrayList<Integer>();
        if (num == null || num.length == 0 || size == 0 || size > num.length)
            return ret;
        ArrayDeque<Integer> deque = new ArrayDeque<Integer>();
        for (int i = 0; i < num.length; i++) {
            int begin = i - size + 1;
            if (deque.isEmpty())
                deque.add(i);
            else if (begin > deque.getFirst()) {
                deque.removeFirst();
            }

            while (!deque.isEmpty() && num[i] >= num[deque.getLast()])
                deque.removeLast();
            deque.add(i);
            if (begin >= 0)
                ret.add(num[deque.getFirst()]);

        }
        return ret;
    }
```

# @ 60. n 个骰子的点数

[Lintcode](https://www.lintcode.com/en/problem/dices-sum/)

**题目描述**

把 n 个骰子扔在地上，求点数和为 s 的概率。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/195f8693-5ec4-4987-8560-f25e365879dd.png" width="300px"> </div><br>
**解题思路**

动态规划

使用一个二维数组 dp 存储点数出现的次数，其中 dp[i][j] 表示前 i 个骰子产生点数 j 的次数。

空间复杂度：O(N<sup>2</sup>)

```java
//求的是 s=1*n  到  s = 6*n的概率的list.Map
public List<Map.Entry<Integer, Double>> dicesSum(int n) {
    final int face = 6;
    final int pointNum = face * n;
    long[][] dp = new long[n + 1][pointNum + 1];

    for (int i = 1; i <= face; i++)
        dp[1][i] = 1;

    for (int i = 2; i <= n; i++)
        for (int j = i; j <= pointNum; j++)     /* 使用 i 个骰子最小点数为 i */
            for (int k = 1; k <= face && k <= j; k++)
                dp[i][j] += dp[i - 1][j - k];

    final double totalNum = Math.pow(6, n);
    List<Map.Entry<Integer, Double>> ret = new ArrayList<>();
    for (int i = n; i <= pointNum; i++)
        ret.add(new AbstractMap.SimpleEntry<>(i, dp[n][i] / totalNum));

    return ret;
}
```

动态规划 + 旋转数组

空间复杂度：O(N)

```java
public List<Map.Entry<Integer, Double>> dicesSum(int n) {
    final int face = 6;
    final int pointNum = face * n;
    long[][] dp = new long[2][pointNum + 1];

    for (int i = 1; i <= face; i++)
        dp[0][i] = 1;

    int flag = 1;                                     /* 旋转标记 */
    for (int i = 2; i <= n; i++, flag = 1 - flag) {
        for (int j = 0; j <= pointNum; j++)
            dp[flag][j] = 0;                          /* 旋转数组清零 */

        for (int j = i; j <= pointNum; j++)
            for (int k = 1; k <= face && k <= j; k++)
                dp[flag][j] += dp[1 - flag][j - k];
    }

    final double totalNum = Math.pow(6, n);
    List<Map.Entry<Integer, Double>> ret = new ArrayList<>();
    for (int i = n; i <= pointNum; i++)
        ret.add(new AbstractMap.SimpleEntry<>(i, dp[1 - flag][i] / totalNum));

    return ret;
}
```

# @61. 扑克牌顺子

[NowCoder](https://www.nowcoder.com/practice/762836f4d43d43ca9deb273b3de8e1f4?tpId=13&tqId=11198&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

五张牌，其中大小王用数字0表示,但是可以代表任何数字。判断这五张牌是否能组成顺子。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/eaa506b6-0747-4bee-81f8-3cda795d8154.png" width="350px"> </div><br>
**解题思路**

```java
改进:
思路不难,小心的是 deta 可能让count(癞子数量)正负抵消. 当numbers[i + 1] == numbers[i],deta为负
    							当numbers[i + 1] > numbers[i]+1 deta为正
    public boolean isContinuous(int[] numbers) {
        if (numbers == null || numbers.length == 0) {
            return false;
        }
        Arrays.sort(numbers);
        int count = 0;
        for (int temp : numbers) {
            if (temp == 0) {
                count++;
            }
        }
        for (int i = 0; i <= numbers.length - 2; i++) {
            if (numbers[i] == 0) {
                continue;
            }
            if (numbers[i + 1] == numbers[i]) {
                return false;
            } else {
                int deta = numbers[i + 1] - 1 - numbers[i];
                count -= deta;
                if (count < 0) {
                    return false;
                }
            }
        }
        return true;
    }

```

# @62. 圆圈中最后剩下的数(约瑟夫环)

[NowCoder](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&tqId=11199&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

让小朋友们围成一个大圈。然后，随机指定一个数 m，让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌，然后可以在礼品箱中任意的挑选礼物，并且不再回到圈中，从他的下一个小朋友开始，继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友，可以不用表演。

**解题思路**

约瑟夫环，圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈，所以最后需要对 n 取余。

```java
改进:
初始位置是begin = 0;
每一次被点到的同学的位置是(begin+m-1)%list.size();
begin是开始位置, 往后移动m-1

public int LastRemaining_Solution(int n, int m) {
        if (n <= 0) {
            return -1;
        }
        LinkedList<Integer> list = new LinkedList<Integer>();
        for (int i = 0; i < n; i++) {
            list.add(i);
        }
        int begin = 0;
        while (list.size() > 1) {
            bt = (begin + m - 1) % n;
            list.remove(begin);
            n = n - 1;
        }
        return list.get(0);
    }
```

#  63. 股票的最大利润

[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/)

**题目描述**

可以有一次买入和一次卖出，买入必须在前。求最大收益。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/42661013-750f-420b-b3c1-437e9a11fb65.png" width="220px"> </div><br>
**解题思路**

使用贪心策略，假设第 i 轮进行卖出操作，买入操作价格应该在 i 之前并且价格最低。

```java
 public int maxProfit(int[] prices) {
        if(prices==null||prices.length==0){
            return 0;
        }
        int len = prices.length;
        int[][] dp = new int[len][2];
        dp[0][0] = 0;
        dp[0][1]=-prices[0];
        for(int i = 1;i<len;i++){
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
        }
        return dp[len-1][0];
    }

public int maxProfit(int[] prices) {
    if (prices == null || prices.length == 0)
        return 0;
    int soFarMin = prices[0];
    int maxProfit = 0;
    for (int i = 1; i < prices.length; i++) {
        soFarMin = Math.min(soFarMin, prices[i]);
        maxProfit = Math.max(maxProfit, prices[i] - soFarMin);
    }
    return maxProfit;
}
改进:
动态规划问题.
    第i天的获利 =  max(   第i-1天卖出+第i到第i-1天的利润(i-1天买入,第i天卖出)   ,    0       )
    dp[i] = max(0,dp[i-1]+diff[i])

  public int maxProfit(int[] prices) {
        int  len = prices.length;
        int[] diff = new int[len];
        for(int i = 1;i<len;i++){
            diff[i] =prices[i]-prices[i-1];
        }
        int[] dp = new int[len];
        int maxProfit = 0;
        for(int i = 1;i<len;i++){
            dp[i] = Math.max(0,dp[i-1]+diff[i]);
            maxProfit = Math.max(maxProfit,dp[i]);
        }
        return maxProfit;

    }

改进:
当只与前一天的状态有关的时候,dp数组可以用一个int来表示

public int maxProfit1(int[] prices) {
        int len = prices.length;
        int[] diff = new int[len];
        for (int i = 1; i < len; i++) {
            diff[i] = prices[i] - prices[i - 1];
        }
        int[] dp = new int[len];
        int maxProfit = 0;
        int last = 0;
        for (int i = 1; i < len; i++) {
            last = Math.max(0, last + diff[i]);
            maxProfit = Math.max(maxProfit, last);
        }
        return maxProfit;
    }
```

# @@64. 求 1+2+3+...+n

[NowCoder](https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 A ? B : C。

**解题思路**

使用递归解法最重要的是指定返回条件，但是本题无法直接使用 if 语句来指定返回条件。

条件与 && 具有短路原则，即在第一个条件语句为 false 的情况下不会去执行第二个条件语句。利用这一特性，将递归的返回条件取非然后作为 && 的第一个条件语句，递归的主体转换为第二个条件语句，那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分，递归返回。

本题的递归返回条件 n > 0；递归的主体部分为 sum += Sum_Solution(n - 1)，转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。

```java
 public int Sum_Solution(int n) {
        int sum = n;
        boolean b = n > 0 && ((sum += Sum_Solution(n - 1)) > 0);
        return sum;
    }
```

# @@65. 不用加减乘除做加法

@num2不要写成>0  因为 进位如果是 1001 其实是一个负数. 这个时候进位是没用完的,但是不符合大于0的要求了

[NowCoder](https://www.nowcoder.com/practice/59ac416b4b944300b617d4f7f111b215?tpId=13&tqId=11201&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

写一个函数，求两个整数之和，要求不得使用 +、-、\*、/ 四则运算符号。

**解题思路**

a ^ b 表示没有考虑进位的情况下两数的和，(a & b) << 1 就是进位。

递归会终止的原因是 (a & b) << 	1 最右边会多一个 0，那么继续递归，进位最右边的 0 会慢慢增多，最后进位会变为 0，递归终止。

```java
 public int Add(int num1, int num2) {
        while (num2 != 0) {
            int xiangJia = num1 ^ num2; //异或求出来的是每一位相加的结果
            int jinWei = (num1 & num2) << 1;  //与 +  左移一位   求出来的是每一位的进位情况

            num1 = xiangJia;
            num2 = jinWei;
        }
        return num1;
    }
```

# 66. 构建乘积数组

[NowCoder](https://www.nowcoder.com/practice/94a4d381a68b47b7a8bed86f2975db46?tpId=13&tqId=11204&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

给定一个数组 A[0, 1,..., n-1]，请构建一个数组 B[0, 1,..., n-1]，其中 B 中的元素 B[i]=A[0]\*A[1]\*...\*A[i-1]\*A[i+1]\*...\*A[n-1]。要求不能使用除法。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4240a69f-4d51-4d16-b797-2dfe110f30bd.png" width="250px"> </div><br>
**解题思路**

```java
 改进: 第i个数组 等于 i左边所有数字的累乘  再乘以i右边数字的累乘

public int[] multiply(int[] A) {
        int len = A.length;
        if (A == null || len == 0) {
            return null;
        }
        int[] B = new int[len];
        int base = 1;
        B[0] = 1;
        for (int i = 0; i < len - 1; i++) {
            base *= A[i];
            B[i + 1] = base;
        }
        base = 1;
        for (int i = len - 1; i >= 1; i--) {
            base *= A[i];
            B[i - 1] *= base;
        }
        return B;
    }
```

# @@@67. 把字符串转换成整数

[NowCoder](https://www.nowcoder.com/practice/1277c681251b4372bdef344468e4f26e?tpId=13&tqId=11202&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)

**题目描述**

将一个字符串转换成一个整数，字符串不是一个合法的数值则返回 0，要求不能使用字符串转换整数的库函数。

```html
Iuput:
+2147483647
1a33

Output:
2147483647
0
```

**解题思路**

在越界的情况下,2147483647比-2147483648要小!!!

![](/Users/sunxu/mycode/Java-notes/notes/pictures/jianzhi-offer/Snipaste_2020-01-05_14-03-41.png)

```java
改进:
字符串转换成int 要注意 1.越界问题,如果越界正负号会改变   2.+0,-0(在我的代码里没考虑也没问题) 3.只有正负号的问题

思路:判断有无正负号,如果是正数,边界是-Integer.MAX_VALUE,即正数的最大值的相反数!!!!
    如果是负数,边界是Integer.MIN_VALUE
    然后无论正负,都按照负数的计算方式来计算result.
    同时用下面两个方法来判断是否越界
    littleLimit = limit/10;
    if (result < littleLimit)
                return 0;
    if (result * 10 < limit + digit)
        //注意!!!!!!!
        //这里不可以写result*10-digit < limit  左边会越界,影响比较大小的真实逻辑
        return 0;

 public int StrToInt(String str) {
        if (str == null || str.length() == 0)
            return 0;
        int len = str.length();
        boolean isNegative = false;
        boolean firstSign = false;
        char[] chars = str.toCharArray();

        int limit = 0;//用正数最大值的相反数作限制
        if (chars[0] == '-') {
            isNegative = true;
            firstSign = true;
            limit = Integer.MIN_VALUE;
        } else if (chars[0] == '+') {
            isNegative = false;
            firstSign = true;
            limit = -Integer.MAX_VALUE;
        } else {
            isNegative = false;
            firstSign = false;
            limit = -Integer.MAX_VALUE;
        }

        int littleLimit = limit / 10;
        int digit = 0;
        int result = 0;
        for (int i = firstSign ? 1 : 0; i < len; i++) {
            if (!(chars[i] >= '0' && chars[i] <= '9'))
                return 0;
            digit = chars[i] - '0';
            if (result < littleLimit)
                return 0;
            if (result * 10 < limit + digit)
                //注意!!!!!!!改进!!!!!!
                //这里不可以写result*10-digit < limit  左边会越界,影响比较大小的真实逻辑
                return 0;
            result = result * 10 - digit;
        }
        return isNegative ? result : -result;

    }
```

# @@68. 树中两个节点的最低公共祖先

**解题思路**

### 二叉查找树

[Leetcode : 235. Lowest Common Ancestor of a Binary Search Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/)

二叉查找树中，两个节点 p, q 的公共祖先 root 满足 root.val >= p.val && root.val <= q.val。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/047faac4-a368-4565-8331-2b66253080d3.jpg" width="220"/> </div><br>
```java
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null)
        return root;
    if (root.val > p.val && root.val > q.val)
        return lowestCommonAncestor(root.left, p, q);
    if (root.val < p.val && root.val < q.val)
        return lowestCommonAncestor(root.right, p, q);
    return root;
}
改进 :迭代方法

     public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null)
            return null;
        int pVal = p.val;
        int qVal = q.val;
        TreeNode node = root;
        while (node != null) {
            if (node.val > pVal && node.val > qVal)
                node = node.left;
            else if (node.val < pVal && node.val < qVal)
                node = node.right;
            else
                return node;
        }
        return node;
    }
```

### 普通二叉树

[Leetcode : 236. Lowest Common Ancestor of a Binary Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/)

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d27c99f0-7881-4f2d-9675-c75cbdee3acd.jpg" width="250"/> </div><br>
```java
  改进:
	在左右子树中查找是否存在 p 或者 q，如果 p 和 q 分别在两个子树中，那么就说明根节点就是最低公共祖先。

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null||root==p||root==q)
            return root;
        TreeNode left = lowestCommonAncestor(root.left,p,q);
        TreeNode right = lowestCommonAncestor(root.right,p,q);
        if(left==null){
            return right;
        }else if(left!=null){
            if(right==null)
                 return left;
            else
                return root;
        }
    }
改进:
非递归的办法, 用stack来遍历整个树,用parentMap来记录路径,用qParentSet和parentMap来存储q的路径节点, 用parentMap来逐层向上得到p的父亲节点,并看看是否在qParentSet里面

 public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null)
            return null;
        Stack<TreeNode> stack = new Stack<>();
        HashMap<TreeNode, TreeNode> parentMap = new HashMap<>();
        parentMap.put(root, null);
        stack.push(root);
        boolean findP = false;
        boolean findQ = false;
        while (!stack.isEmpty()) {
            //
            root = stack.pop();

            if (root == p)
                findP = true;
            else if (root == q)
                findQ = true;

            if (findP && findQ) {
                HashSet<TreeNode> qParent = new HashSet<>();
                while (q != null) {
                    qParent.add(q);
                    q = parentMap.get(q);
                }
                while (p != null) {
                    if (qParent.contains(p)) {
                        return p;
                    }
                    p = parentMap.get(p);
                }
            }

            if (root.right != null) {
                parentMap.put(root.right, root);
                stack.push(root.right);
            }
            if (root.left != null) {
                parentMap.put(root.left, root);
                stack.push(root.left);
            }
        }
        return null;
    }

```

# 70.对称的二叉树

```java
改进思路  private boolean isSymmerical(TreeNode r1, TreeNode r2) {

boolean isSymmetrical(TreeNode pRoot) {
        return isSymmerical(pRoot, pRoot);
    }

    private boolean isSymmerical(TreeNode r1, TreeNode r2) {
        if (r1 == null && r1 == null) {
            return true;
        } else if (r1 == null || r2 == null) {
            return false;
        }
        if (r1.val == r2.val) {
            return isSymmerical(r1.left, r2.right) && isSymmerical(r1.right, r2.left);
        }
        return false;
    }

非递归的写法
    改进思路  	     queue.add(left.left);
                    queue.add(right.right);
                    queue.add(left.right);
                    queue.add(right.left);

    boolean isSymmetrical(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        if (root == null)
            return true;
        queue.add(root.left);
        queue.add(root.right);
        while (!queue.isEmpty()) {
            TreeNode left = queue.poll();
            TreeNode right = queue.poll();
            if (left == null && right == null) {

            } else if (left != null && right != null) {
                if (left.val != right.val) {
                    return false;
                } else {
                    queue.add(left.left);
                    queue.add(right.right);
                    queue.add(left.right);
                    queue.add(right.left);
                }
            } else {
                return false;
            }
        }
        return true;
    }

```

# @@71.数据流中的中位数

https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

```java
改进思路:
以中位数为界,大顶堆装左边的数,小顶堆装右边的数.

Comparator<Integer> c1 = new Comparator<Integer>() {
        @Override
        public int compare(Integer i1, Integer i2) {
            return i1 - i2;//从小到大
        }
    };
    Comparator<Integer> c2 = new Comparator<Integer>() {
        public int compare(Integer i1, Integer i2) {
            return i2 - i1;
        }
    };
    private PriorityQueue<Integer> daDingDui = new PriorityQueue<>(16, c2);
    private PriorityQueue<Integer> xiaoDingdui = new PriorityQueue<>(16, c1);
    int count = 0;

    public void Insert(Integer num) {
        //num的奇偶
        count++;
        if (count % 2 == 0) {
            //往1上面插数据
            daDingDui.add(num);
            xiaoDingdui.add(daDingDui.poll());

        } else {
            //往2上查数据
            xiaoDingdui.add(num);
            daDingDui.add(xiaoDingdui.poll());
        }
    }

    public Double GetMedian() {

        if (count % 2 == 0) {
            return (double) (daDingDui.peek() + xiaoDingdui.peek()) / 2;
        } else {
            return (double) daDingDui.peek();
        }

    }
```

<file_sep>/notes/LeetCode03.md
<!-- TOC -->

- [动态规划和贪心算法的区别](#动态规划和贪心算法的区别)
- [[@@@@72. 编辑距离](https://leetcode-cn.com/problems/edit-distance/)](#72-编辑距离httpsleetcode-cncomproblemsedit-distance)
- [[@1143. 最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/)](#1143-最长公共子序列httpsleetcode-cncomproblemslongest-common-subsequence)
- [@删除最少字符变成回文串](#删除最少字符变成回文串)
- [[@@647. 回文子串](https://leetcode-cn.com/problems/palindromic-substrings/)(数量)](#647-回文子串httpsleetcode-cncomproblemspalindromic-substrings数量)
- [[@@5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/)(长度)](#5-最长回文子串httpsleetcode-cncomproblemslongest-palindromic-substring长度)
- [用少来表示多](#用少来表示多)
- [@[253. 会议室 II](https://leetcode-cn.com/problems/meeting-rooms-ii/)](#253-会议室-iihttpsleetcode-cncomproblemsmeeting-rooms-ii)
- [@@@[435. 无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/)](#435-无重叠区间httpsleetcode-cncomproblemsnon-overlapping-intervals)
- [[@@@@@452. 用最少数量的箭引爆气球](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/)](#452-用最少数量的箭引爆气球httpsleetcode-cncomproblemsminimum-number-of-arrows-to-burst-balloons)
- [[443. 压缩字符串](https://leetcode-cn.com/problems/string-compression/)](#443-压缩字符串httpsleetcode-cncomproblemsstring-compression)
- [@@@[43. 字符串相乘](https://leetcode-cn.com/problems/multiply-strings/)](#43-字符串相乘httpsleetcode-cncomproblemsmultiply-strings)
- [字符移位](#字符移位)
- [[295. 数据流的中位数](https://leetcode-cn.com/problems/find-median-from-data-stream/)](#295-数据流的中位数httpsleetcode-cncomproblemsfind-median-from-data-stream)
- [@@@@图的m着色问题(dfs)](#图的m着色问题dfs)
- [](#)
- [100亿数字排序,求中位数,平均数,求和](#100亿数字排序求中位数平均数求和)
    - [方法1](#方法1)
    - [方法2:](#方法2)
    - [思维拓展](#思维拓展)
- [[113. 路径总和 II](https://leetcode-cn.com/problems/path-sum-ii/)](#113-路径总和-iihttpsleetcode-cncomproblemspath-sum-ii)
- [[@@@547. 朋友圈](https://leetcode-cn.com/problems/friend-circles/)](#547-朋友圈httpsleetcode-cncomproblemsfriend-circles)
- [[@@@200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/)](#200-岛屿数量httpsleetcode-cncomproblemsnumber-of-islands)
- [[@@@1319. 连通网络的操作次数](https://leetcode-cn.com/problems/number-of-operations-to-make-network-connected/)](#1319-连通网络的操作次数httpsleetcode-cncomproblemsnumber-of-operations-to-make-network-connected)
- [找到二叉搜索树中与target之差的绝对值最小的node](#找到二叉搜索树中与target之差的绝对值最小的node)
- [@@@@二叉树的非递归后序遍历](#二叉树的非递归后序遍历)
- [@@@有序数组重建平衡二叉树](#有序数组重建平衡二叉树)
- [一个数组里找任意两个数之和的绝对值最小值](#一个数组里找任意两个数之和的绝对值最小值)
- [一个整数数组找任意两数的最小差](#一个整数数组找任意两数的最小差)
- [[@@208. 实现 Trie (前缀树)](https://leetcode-cn.com/problems/implement-trie-prefix-tree/)](#208-实现-trie-前缀树httpsleetcode-cncomproblemsimplement-trie-prefix-tree)
- [[@209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/)](#209-长度最小的子数组httpsleetcode-cncomproblemsminimum-size-subarray-sum)
- [[@@60. 第k个排列](https://leetcode-cn.com/problems/permutation-sequence/)](#60-第k个排列httpsleetcode-cncomproblemspermutation-sequence)
- [[@@@@162. 寻找峰值](https://leetcode-cn.com/problems/find-peak-element/)](#162-寻找峰值httpsleetcode-cncomproblemsfind-peak-element)
- [[@@@@127. 单词接龙](https://leetcode-cn.com/problems/word-ladder/)](#127-单词接龙httpsleetcode-cncomproblemsword-ladder)
- [IP重复攻击](#ip重复攻击)
- [ab替换成c,b替换成ef](#ab替换成cb替换成ef)
- [[@@@4. 寻找两个有序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/)](#4-寻找两个有序数组的中位数httpsleetcode-cncomproblemsmedian-of-two-sorted-arrays)
- [[@@@@@701. 二叉搜索树中的插入操作](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/)](#701-二叉搜索树中的插入操作httpsleetcode-cncomproblemsinsert-into-a-binary-search-tree)
- [[@@@@@@@@@@450. 删除二叉搜索树中的节点](https://leetcode-cn.com/problems/delete-node-in-a-bst/)](#450-删除二叉搜索树中的节点httpsleetcode-cncomproblemsdelete-node-in-a-bst)
- [[654. 最大二叉树](https://leetcode-cn.com/problems/maximum-binary-tree/)](#654-最大二叉树httpsleetcode-cncomproblemsmaximum-binary-tree)
- [[@@@273. 整数转换英文表示](https://leetcode-cn.com/problems/integer-to-english-words/)](#273-整数转换英文表示httpsleetcode-cncomproblemsinteger-to-english-words)
- [[272. 最接近的二叉搜索树值 II](https://leetcode-cn.com/problems/closest-binary-search-tree-value-ii/)](#272-最接近的二叉搜索树值-iihttpsleetcode-cncomproblemsclosest-binary-search-tree-value-ii)
- [@@@**拓扑排序**](#拓扑排序)
- [[@@210. 课程表 II](https://leetcode-cn.com/problems/course-schedule-ii/)](#210-课程表-iihttpsleetcode-cncomproblemscourse-schedule-ii)
- [[@207. 课程表](https://leetcode-cn.com/problems/course-schedule/)](#207-课程表httpsleetcode-cncomproblemscourse-schedule)
- [[@leetcode.298 二叉树的最长连续子序列](https://blog.csdn.net/jmspan/article/details/51171217)](#leetcode298-二叉树的最长连续子序列httpsblogcsdnnetjmspanarticledetails51171217)
- [[@@面试题51. 数组中的逆序对](https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/)](#面试题51-数组中的逆序对httpsleetcode-cncomproblemsshu-zu-zhong-de-ni-xu-dui-lcof)
- [[@@63. 不同路径 II](https://leetcode-cn.com/problems/unique-paths-ii/)](#63-不同路径-iihttpsleetcode-cncomproblemsunique-paths-ii)
- [[单位圆能框住的最多节点数](https://blog.csdn.net/Ramay7/article/details/51147778)](#单位圆能框住的最多节点数httpsblogcsdnnetramay7articledetails51147778)
- [[@@@四种操作求A的最大次数](https://blog.csdn.net/qq_40147449/article/details/93376889)](#四种操作求a的最大次数httpsblogcsdnnetqq_40147449articledetails93376889)
- [点集旋转](#点集旋转)
- [[@@面试题 16.06. 最小差](https://leetcode-cn.com/problems/smallest-difference-lcci/)](#面试题-1606-最小差httpsleetcode-cncomproblemssmallest-difference-lcci)
- [[@670. 最大交换](https://leetcode-cn.com/problems/maximum-swap/)](#670-最大交换httpsleetcode-cncomproblemsmaximum-swap)
- [[@@@@50. Pow(x, n)](https://leetcode-cn.com/problems/powx-n/)](#50-powx-nhttpsleetcode-cncomproblemspowx-n)
- [[@@89. 格雷编码](https://leetcode-cn.com/problems/gray-code/)](#89-格雷编码httpsleetcode-cncomproblemsgray-code)
- [[@53. 最大子序和](https://leetcode-cn.com/problems/maximum-subarray/)](#53-最大子序和httpsleetcode-cncomproblemsmaximum-subarray)
- [单例模式](#单例模式)
- [[@@@@@752. 打开转盘锁](https://leetcode-cn.com/problems/open-the-lock/)](#752-打开转盘锁httpsleetcode-cncomproblemsopen-the-lock)
- [[掷骰子的数学期望](https://www.zhihu.com/question/30470526)](#掷骰子的数学期望httpswwwzhihucomquestion30470526)
- [[915. 分割数组](https://leetcode-cn.com/problems/partition-array-into-disjoint-intervals/)](#915-分割数组httpsleetcode-cncomproblemspartition-array-into-disjoint-intervals)
- [[@@@@991. 坏了的计算器](https://leetcode-cn.com/problems/broken-calculator/)](#991-坏了的计算器httpsleetcode-cncomproblemsbroken-calculator)
- [[@@@@@227. 基本计算器 II](https://leetcode-cn.com/problems/basic-calculator-ii/)](#227-基本计算器-iihttpsleetcode-cncomproblemsbasic-calculator-ii)
- [[@@删除回文子序列](https://leetcode-cn.com/problems/remove-palindromic-subsequences/)](#删除回文子序列httpsleetcode-cncomproblemsremove-palindromic-subsequences)
- [@@@@[Leetcode 1246：删除回文子数组](https://coordinate.wang/index.php/archives/2737/)](#leetcode-1246删除回文子数组httpscoordinatewangindexphparchives2737)
- [[328. 奇偶链表](https://leetcode-cn.com/problems/odd-even-linked-list/)](#328-奇偶链表httpsleetcode-cncomproblemsodd-even-linked-list)
- [[@@179. 最大数](https://leetcode-cn.com/problems/largest-number/)](#179-最大数httpsleetcode-cncomproblemslargest-number)
- [[@@@69. x 的平方根](https://leetcode-cn.com/problems/sqrtx/)](#69-x-的平方根httpsleetcode-cncomproblemssqrtx)
- [@@@@@@从十亿数字中,求第k位数字,用快排.](#从十亿数字中求第k位数字用快排)
- [[@@973. 最接近原点的 K 个点](https://leetcode-cn.com/problems/k-closest-points-to-origin/)](#973-最接近原点的-k-个点httpsleetcode-cncomproblemsk-closest-points-to-origin)
- [[1375. 灯泡开关 III](https://leetcode-cn.com/problems/bulb-switcher-iii/)](#1375-灯泡开关-iiihttpsleetcode-cncomproblemsbulb-switcher-iii)
- [[@@581. 最短无序连续子数组](https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/)](#581-最短无序连续子数组httpsleetcode-cncomproblemsshortest-unsorted-continuous-subarray)
- [[@@@@@@769. 最多能完成排序的块](https://leetcode-cn.com/problems/max-chunks-to-make-sorted/)](#769-最多能完成排序的块httpsleetcode-cncomproblemsmax-chunks-to-make-sorted)
- [@@元素的技巧(如果大于左边最大值,小于右边最小值,该元素排序后依然在该位置上)](#元素的技巧如果大于左边最大值小于右边最小值该元素排序后依然在该位置上)
- [@@缝隙的技巧(如果大于前缀最大值,小于后缀最小值,则是分界元素)](#缝隙的技巧如果大于前缀最大值小于后缀最小值则是分界元素)
- [@@@@划分最多的最短无序连续子数组](#划分最多的最短无序连续子数组)
- [[@@292. Nim 游戏](https://leetcode-cn.com/problems/nim-game/)](#292-nim-游戏httpsleetcode-cncomproblemsnim-game)
- [[36. 有效的数独](https://leetcode-cn.com/problems/valid-sudoku/)](#36-有效的数独httpsleetcode-cncomproblemsvalid-sudoku)
- [[150. 逆波兰表达式求值](https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/)](#150-逆波兰表达式求值httpsleetcode-cncomproblemsevaluate-reverse-polish-notation)
- [[@@@@659. 分割数组为连续子序列](https://leetcode-cn.com/problems/split-array-into-consecutive-subsequences/)](#659-分割数组为连续子序列httpsleetcode-cncomproblemssplit-array-into-consecutive-subsequences)
- [[@@@@@@877. 石子游戏](https://leetcode-cn.com/problems/stone-game/)](#877-石子游戏httpsleetcode-cncomproblemsstone-game)
- [[@@编程算法题—硬币游戏](https://blog.csdn.net/guangyacyb/article/details/80173509)](#编程算法题硬币游戏httpsblogcsdnnetguangyacybarticledetails80173509)
- [[5369. 统计作战单位数](https://leetcode-cn.com/problems/count-number-of-teams/)](#5369-统计作战单位数httpsleetcode-cncomproblemscount-number-of-teams)
- [[5370. 设计地铁系统](https://leetcode-cn.com/problems/design-underground-system/)](#5370-设计地铁系统httpsleetcode-cncomproblemsdesign-underground-system)
- [海量数据的排序](#海量数据的排序)
- [[@@@146. LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/)](#146-lru缓存机制httpsleetcode-cncomproblemslru-cache)
- [[@@@@140. 单词拆分 II](https://leetcode-cn.com/problems/word-break-ii/)](#140-单词拆分-iihttpsleetcode-cncomproblemsword-break-ii)
- [把字符串中重复的子串删除](#把字符串中重复的子串删除)
- [时光倒流](#时光倒流)
- [正四面体游走](#正四面体游走)

<!-- /TOC -->

#### 动态规划和贪心算法的区别

动态规划和贪心算法都是一种递推算法
均有局部最优解来推导全局最优解

不同点：
贪心算法：
1.贪心算法中，作出的每步贪心决策都无法改变，因为贪心策略是由上一步的最优解推导下一步的最优解，而上一部之前的最优解则不作保留。
2.由（1）中的介绍，可以知道贪心法正确的条件是：每一步的最优解一定包含上一步的最优解。

#### [@@@@72. 编辑距离](https://leetcode-cn.com/problems/edit-distance/)

@如果有一个字符串长度为0

@初始化边界的方式

@如果i和j相等,取的不是 `math.min(dp[i][j],dp[i-1][j-1])`;而是直接取`dp[i-1][j-1],因为dp[i][j]此时为0`

@@@@@@@删除 插入 替换都是针对word1而言的

```
讲一下我自己对状态转移方程的理解,麻烦大家看看我说得对不对有没有道理:
(一)、当word1[i]==word2[j]时,由于遍历到了i和j,说明word1的0~i-1和word2的0~j-1的匹配结果已经生成,
由于当前两个字符相同,因此无需做任何操作,dp[i][j]=dp[i-1][j-1]
(二)、当word1[i]!=word2[j]时,可以进行的操作有3个:
      ① 替换操作:可能word1的0~i-1位置与word2的0~j-1位置的字符都相同,
           只是当前位置的字符不匹配,进行替换操作后两者变得相同,
           所以此时dp[i][j]=dp[i-1][j-1]+1(这个加1代表执行替换操作)
      ②删除操作:若此时word1的0~i-1位置与word2的0~j位置已经匹配了,
         此时多出了word1的i位置字符,应把它删除掉,才能使此时word1的0~i(这个i是执行了删除操作后新的i)
         和word2的0~j位置匹配,因此此时dp[i][j]=dp[i-1][j]+1(这个加1代表执行删除操作)
      ③插入操作:若此时word1的0~i位置只是和word2的0~j-1位置匹配,
          此时只需要在原来的i位置后面插入一个和word2的j位置相同的字符使得
          此时的word1的0~i(这个i是执行了插入操作后新的i)和word2的0~j匹配得上,
          所以此时dp[i][j]=dp[i][j-1]+1(这个加1代表执行插入操作)
      ④由于题目所要求的是要最少的操作数:所以当word1[i] != word2[j] 时,
          需要在这三个操作中选取一个最小的值赋格当前的dp[i][j]
```

```JAVA
public int minDistance(String word1, String word2) {
    if (word1 == null || word2 == null) {
        return 0;
    }
    int len1 = word1.length();
    int len2 = word2.length();
    //如果其中有一个字符串长度为0
    if (len1 * len2 == 0) {
        return len1 + len2;
    }
    int[][] dp = new int[len1 + 1][len2 + 1];
    //边界的初始化
    for (int i = 0; i <= len1; i++) {
        dp[i][0] = i;
    }
    for (int j = 0; j <= len2; j++) {
        dp[0][j] = j;
    }
    //如果i和j的字符相等,则ij的编辑距离等于 i-1 j-1
    //如果i j的字符不等,则ij的编辑距离取决于删除i-1 还是删除 j-1的字符会让编辑距离小一点
    for (int i = 1; i <= len1; i++) {
        for (int j = 1; j <= len2; j++) {
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }
        }
    }
    return dp[len1][len2];
}
```

#### [@1143. 最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/)

```java
public int longestCommonSubsequence(String text1, String text2) {
        if (text1 == null || text2 == null) {
            return 0;
        }
        int len1 = text1.length();
        int len2 = text2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    //如果此处的字符不匹配,则考虑删除t1一个 或者 t2一个
                    dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
                }
            }
        }
        return dp[len1][len2];
    }
```

#### @删除最少字符变成回文串

<img src="pictures/LeetCode03/image-20200309172717801.png" alt="image-20200309172717801" style="zoom:50%;" />

题目描述：给定一字符串s,求最少删除多少个字符可以使得s成为回文串。例如：s="abca",答案是1.

思路:把自身翻转, 自身和翻转的lcs,就是最大回文长度,str.length()-lcs 即为要删除的字符.

```java
public int minChange(String str) {
        int lcs = longestCommonSubsequence(str, null);
        return str.length() - lcs;
    }

    public int longestCommonSubsequence(String text1, String text2) {
        int len1 = text1.length();
        StringBuffer stringBuffer = new StringBuffer(text1);
        text2 = stringBuffer.reverse().toString();
        int len2 = text2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for (int i = 0; i <= len1; i++) {
            for (int j = 0; j <= len2; j++) {
                if (j == 0 || i == 0) {
                    dp[i][j] = 0;
                    continue;
                }
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[len1][len2];
    }
```

#### [@@647. 回文子串](https://leetcode-cn.com/problems/palindromic-substrings/)(数量)

本题可以看成是字符串类的动态规划

中心扩展法比动态规划效率高.

因为动态规划是从短串到长串都要判断,不好剪枝

中心扩展从短串到长串的过程中,如果短串不成立,则长串终止.

```java
改进:动态规划的做法
     i-j<2  在i-j为0的时候,i和j为同一个值;i-j为1的时候由于前面的
     s.charAt(i)==s.charAt(j)也决定了它们是相等的
public int countSubstrings(String s) {
    int len = s.length();
    boolean[][] dp = new boolean[len][len];
    int ret = 0;
    for (int i = 0; i < len; i++) {
        for (int j = i; j >= 0; j--) {
            //i-j<2  在i-j为0的时候,i和j为同一个值;i-j为1的时候由于前面的
            //s.charAt(i)==s.charAt(j)也决定了它们是相等的
            if (s.charAt(i) == s.charAt(j) && ((i - j < 2) || dp[i - 1][j + 1])) {
                dp[i][j] = true;
                ret++;
            }
        }
    }
    return ret;
}
改进: 从中间往两边扩展,注意拓展方式有两种,  aba 以b为对称线  和 aa以两个a之间为对称线
    对应的代码是  int left = i;
           		 int right = left;
				left = i;
           		 right = left + 1;

     public int countSubstrings(String s) {
        int len = s.length();
        int res = 0;
        for (int i = 0; i < len; i++) {
            int left = i;
            int right = left;
            while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) {
                left--;
                right++;
                res++;
            }
            left = i;
            right = left + 1;
            while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) {
                left--;
                right++;
                res++;
            }

        }
        return res;
    }
```

#### [@@5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/)(长度)

```java
改进:利用动规:dp[i][j] = dp[i-1][j+1] +1 (如果len>2的情况下,并且char[i] == char[j])
    dp[i][j]的含义是 以为i终点,j为起点的字符串中对称字符的长度.

public String longestPalindrome(String s) {
    if (s == null || s.length() == 0) {
        return "";
    }
    int maxLen = 0;
    int maxRow = 0;
    int maxCol = 0;
    int len = s.length();
    int[][] dp = new int[len][len];
    for (int i = 0; i < len; i++) {
        for (int j = i; j >= 0; j--) {

            if (s.charAt(i) == s.charAt(j)) {
                if (i == j) {//长度为1
                    dp[i][j] = 1;
                } else if (i == j + 1) {//长度为2
                    dp[i][j] = 2;
                } else { //长度大于2
                    if (dp[i - 1][j + 1] != 0) {
                        dp[i][j] = dp[i - 1][j + 1] + 2;

                    } else {
                        dp[i][j] = 0;
                    }
                }
                if (maxLen < dp[i][j]) {
                    maxLen = dp[i][j];
                    maxRow = i;
                    maxCol = j;
                }
            } else {
                //char i  与 char j 不相等
                dp[i][j] = 0;
            }
        }
    }
    return s.substring(maxCol, maxRow + 1);
}
```

#### 用少来表示多

给一串只包含0~9的数字串，每个数字出现的概率相同（比如32978417506），现在告诉你（1,3,5,7）这四个数字不可用，即只能用（0,2,4,6,8,9）这6个数，如何表示原数字串？

* **00表示0**, 02表示1, 04表示3， 06表示5， 08表示7

* 15位表示以前的10位，所以存储多出来0.5倍

#### @[253. 会议室 II](https://leetcode-cn.com/problems/meeting-rooms-ii/)

```java
思路:startTime排序,每到一个startTime,usedRoom就+1.如果在该时间之前有结束的会议室(endTime[endPointer]<=startTime[startPointer]).usedRoom就-1;
public int minMeetingRooms(int[][] intervals) {
        int len = intervals.length;

        int max = 0;
        int[] startTime = new int[len];
        int[] endTime = new int[len];
        for (int i = 0; i < len; i++) {
            startTime[i] = intervals[i][0];
            endTime[i] = intervals[i][1];
        }
        Arrays.sort(startTime);
        Arrays.sort(endTime);
        int usedRooms = 0;
        int endPointer = 0;
        for (int startPointer = 0; startPointer < len; ) {
            if (endTime[endPointer] <= startTime[startPointer]) {
                usedRooms--;
                endPointer++;
            }
            usedRooms++;
            startPointer++;
        }
        return usedRooms;
    }
```

#### @@@[435. 无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/)

求无重叠区间cnt,然后用len-ret就是需要移除的最少区间数

把区间按照endTime从小到大排序. 然后往后找,如果后面的区间的startTime<该endTime,则有重叠.

如果endTIme>=startTime无重叠,此时endTime更新到下个区间,cnt++

 @按照开始时间排序的贪心算法

@@@  //按照开始时间排序,如果有交叉,则如果pre的end比较大,则删除掉pre, pre赋值为i
        //如果pre的end比i的end小,pre还是原来的,删除掉i这个区间

```java
public int eraseOverlapIntervals(int[][] intervals) {
    if (intervals == null) {
        return 0;
    }
    int len = intervals.length;
    Comparator<int[]> c = new Comparator<int[]>() {
        public int compare(int[] i1, int[] i2) {
            return i1[0] - i2[0];
        }
    };
    Arrays.sort(intervals, c);
    int count = 0;
    int pre = 0;
    for (int i = 1; i < len; i++) {
        //如果有交叉,则如果pre的end比较大,则删除掉pre, pre赋值为i
        //如果pre的end比i的end小,pre还是原来的,删除掉i这个区间
        if (intervals[pre][1] > intervals[i][0]) {
            if (intervals[pre][1] > intervals[i][1]) {
                count++;
                pre = i;
            } else {
                count++;
            }
         //如果没交叉,就直接下一个
        } else {
            pre = i;
        }
    }
    return count;
}
```

```java
@按照结束时间排序的  贪心算法
从区间集合 intvs 中选择一个区间 x，这个 x 是在当前所有区间中结束最早的（end 最小）。
把所有与 x 区间相交的区间从区间集合 intvs 中删除。
重复步骤 1 和 2，直到 intvs 为空为止。之前选出的那些 x 就是最大不相交子集。

public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals == null || intervals.length == 0) {
            return 0;
        }
        Comparator<int[]> c = new Comparator<int[]>() {
            public int compare(int[] a, int[] b) {
                return a[1] - b[1];
            }
        };
        int len = intervals.length;
        Arrays.sort(intervals, c);
        int cnt = 1;
        int endTime = intervals[0][1];
        for (int i = 1; i < intervals.length; i++) {
            int startTime = intervals[i][0];
            if (endTime > startTime) {
                continue;
            } else if (endTime <= startTime) {
                cnt++;
                endTime = intervals[i][1];
            }
        }
        return len - cnt;
    }
```

#### [@@@@@452. 用最少数量的箭引爆气球](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/)

@@贪心算法

@把区间按照startTime排序,

先规定一个气球的end.然后找在这个end之内的气球.直到找不到了.

就end改为下一个新气球的end.count++

```java
public int findMinArrowShots(int[][] intervals) {
    if (intervals == null || intervals.length == 0) {
        return 0;
    }
    int len = intervals.length;
    Comparator<int[]> c = new Comparator<int[]>() {
        public int compare(int[] i1, int[] i2) {
            return i1[0] - i2[0];
        }
    };
    Arrays.sort(intervals, c);
    int count = 1;
    int pre = 0;
    int end = intervals[pre][1];
    for (int i = 1; i < intervals.length; i++) {
        if (end >= intervals[i][0]) {
            end = Math.min(end, intervals[i][1]);
        } else if (end < intervals[i][0]) {
            end = intervals[i][1];
            count++;
        }
    }
    return count;
}
```

#### [443. 压缩字符串](https://leetcode-cn.com/problems/string-compression/)

write read,chBegin的妙用

if (read == chars.length - 1 || chars[read] != chars[read + 1])  这两个条件很重要

```java
 public int compress(char[] chars) {
        if (chars == null) {
            return 0;
        }
        int left = 0;
        int write = 0;
        int len = chars.length;
        for (int right = 0; right < len; ) {
            char cur = chars[left];
            while (right + 1 < len && chars[right] == chars[right + 1]) {
                right++;
            }
            chars[write++] = cur;
            int cnt = right - left + 1;
            if (cnt > 1) {
                String temp = cnt + "";
                for (int i = 0; i < temp.length(); i++) {
                    chars[write++] = temp.charAt(i);
                }
            }
            left = right + 1;
            right++;
        }
        return write;
    }
```

#### @@@[43. 字符串相乘](https://leetcode-cn.com/problems/multiply-strings/)

ret的长度是len1+len2  ;  a*b放在的位置是 **i+j+1**

@i 和 j长的数字 乘出来的最大长度是 i+j 所以创建数组 new int[i+j]

 @i和 j 都是从len1-1   len2-1 开始往小走, 乘出来的数据放在i+j+1的地方

```java
 public String multiply(String num1, String num2) {
        if (num1.equals("0") || num2.equals("0")) {
            return "0";
        }
        int[] ret = new int[num1.length() + num2.length()];
        for (int i = num1.length() - 1; i >= 0; i--) {
            int a = num1.charAt(i) - '0';
            for (int j = num2.length() - 1; j >= 0; j--) {

                int b = num2.charAt(j) - '0';
                int sum = a * b + ret[i + j + 1];

                ret[i + j + 1] = sum % 10;
                ret[i + j] += sum / 10;
            }
        }
        StringBuilder retStr = new StringBuilder();
        for (int i = 0; i < ret.length; i++) {
            if (i == 0 && ret[i] == 0) {
                while (ret[i] == 0) {
                    i++;
                }
            }
            retStr.append(ret[i] + "");
        }
        return retStr.toString();
    }
```

#### 字符移位

链接：https://www.nowcoder.com/questionTerminal/7e8aa3f9873046d08899e0b44dac5e43
来源：牛客网

小Q最近遇到了一个难题：把一个字符串的大写字母放到字符串的后面，各个字符的相对位置不变，且不能申请额外的空间。
  你能帮帮小Q吗？

**输入描述:**

```
输入数据有多组，每组包含一个字符串s，且保证:1<=s.length<=1000.

```

**输出描述:**

```
对于每组数据，输出移位后的字符串。
```

```java
思路1
冒泡:
@@@@@@@@@@@@@@@
@要交换 i 0~len-1次
@每次  j从 len-1  到  i进行扫描;逐渐冒泡,把一个小写字母冒到i的位置上.

 public String func(String str) {
        char[] chars = str.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            for (int j = chars.length - 2; j >= i; j--) {
                if (chars[j] >= 'A' && chars[j] <= 'Z' && chars[j + 1] >= 'a' && chars[j + 1] <= 'z') {
                    char temp = chars[j];
                    chars[j] = chars[j + 1];
                    chars[j + 1] = temp;
                }
            }
        }
        return new String(chars);
    }

思路2:
选择排序:
@@@@@@@@@@@@@@@@@@@
@littleIndex代表下一个要插入小写字母的位置
@i往后找,找到一个小写字母. 然后从littleIndex 到 i之间的字母往后移一位
@然后把i处的字母插入到littleIndex处.

 public String func(String str) {
        char[] chars = str.toCharArray();
        int littleIndex = 0;
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] >= 'a' && chars[i] <= 'z') {
                char temp = chars[i];
                for (int j = i; j >= littleIndex + 1; j--) {
                    chars[j] = chars[j - 1];
                }
                chars[littleIndex] = temp;
                littleIndex++;
            }
        }
        return new String(chars);
    }

思路3:

public static String getResult(String str) {
        return str.replaceAll("[A-Z]", "") + str.replaceAll("[a-z]", "");
    }
```

#### [295. 数据流的中位数](https://leetcode-cn.com/problems/find-median-from-data-stream/)

@优先队列是log(n)的复杂度.

@当两个优先队 列中都有数字的时候,新插入的数字无论是插在左边还是右边,都是导致整体无序的隐患,

所以每次插入都需要判断和交换顶部的元素

```java

class MedianFinder {
    PriorityQueue<Integer> leftNums = null;
    PriorityQueue<Integer> rightNums = null;

    public MedianFinder() {
        leftNums = new PriorityQueue<Integer>(10, (o2, o1) -> (o1 - o2));
        rightNums = new PriorityQueue<Integer>(10, (o1, o2) -> (o1 - o2));

    }

    public void addNum(int num) {
        if (leftNums.size() > rightNums.size()) {
            rightNums.add(num);
        } else {
            leftNums.add(num);
        }
        if (!rightNums.isEmpty() && rightNums.peek() < leftNums.peek()) {
            int leftNum = leftNums.poll();
            int rightNum = rightNums.poll();
            leftNums.add(rightNum);
            rightNums.add(leftNum);
        }
    }

    public double findMedian() {
        if (leftNums.size() == rightNums.size()) {
            return (double) (leftNums.peek() + rightNums.peek()) / 2;
        }
        return leftNums.peek();
    }
}
```

#### @@@@图的m着色问题(dfs)

@color数组,0代表还未染色,并且可以用来当visited数组

@先染色,染色后找到第一个未访问的point进行dfs.

@如果图中任意两点都是连通的，那么图被称作连通图。 如果有图如下,5那里在我的代码里不适用原因是

1.我用回溯的cnt作为ret++的条件,应该改成cnt为全量,并且1节点每换一次颜色,cnt置零

2.backtracing往下走的时候,只会选择第一个联通point,后续point会被抛弃,比如图中5

<img src="pictures/LeetCode03/image-20200309235552816.png" alt="image-20200309235552816" style="zoom:50%;" />

[问题描述]

给定无向连通图G和m种不同的颜色，用这些颜色给图的各个顶点着一种颜色，若某种方案使得图中每条边的2个顶点的颜色都不相同，则是一个满足的方案，找出所有的方案。

输入格式
 第一行有3个正整数n，k和m，分别表示n个顶点，k条边，m种颜色
接下来k行，每行2个正整数，保送一条边的两个顶点

所有不同的着色方案数
输入样例】
5 8 4
1 2
1 3
1 4
2 3
2 4
2 5
3 4
4 5
输出样例
48

```java
  public class Main {
    static int[][] matrix = null;
    int[] pointColor = null;
    int len = 0;
    int numPoint = 0;
    int numEdge = 0;
    int ret = 0;
    int numColor = 0;

    public int getResult(int numPoint, int numEdge, int numColor) {
        len = numPoint;
        this.numColor = numColor;
        this.numEdge = numEdge;
        this.numPoint = numPoint;
//        this.matrix = new int[numPoint + 1][numPoint + 1];
        pointColor = new int[numPoint + 1];
        backtracing(1, 1);
        return ret;
    }

    private boolean uniqueColor(int index) {
        for (int i = 1; i <= numPoint; i++) {
            if (matrix[index][i] == 1 && pointColor[index] == pointColor[i]) {
                return false;
            }
        }
        return true;
    }

    private void backtracing(int index, int cnt) {
        if (cnt >= numPoint + 1) {
            return;
        }
        //先染色
        for (int j = 1; j <= numColor; j++) {
            pointColor[index] = j;
            //如果颜色符合要求
            if (uniqueColor(index)) {
                if (cnt == numPoint) {
                    ret++;
                }
                //从与index相连的  第一个没有被访问过的 顶点开始染色
                //为什么是第一个,假设1与2,3相连,我给1染颜色1,2染颜色2,3染颜色3.
                //1->2->3  和  1->3->2  其实是同一种颜色组合.
                for (int k = 1; k <= numPoint; k++) {
                    if (matrix[index][k] == 1 && pointColor[k] == 0) {
                        backtracing(k, cnt + 1);
                        break;
                    }
                }
            }
            pointColor[index] = 0;
        }
    }

    private void initialMatrix(String str) {
        for (int i = 0; i < str.length(); i = i + 2) {
            int x = str.charAt(i) - '0';
            int y = str.charAt(i + 1) - '0';
            matrix[x][y] = 1;
            matrix[y][x] = 1;
        }
    }

    public static void main(String[] args) {
        Main main = new Main();
        String str = "1213142324253445";
        matrix = new int[6][6];
        main.initialMatrix(str);

        int func = main.getResult(5, 8, 4);
        System.out.println(func);

    }
}
```

```java
 下面的做法错误的原因是
 1->2->3和
 1->3->2 假设1涂红,2涂蓝,3涂绿,这被认为是两种情况了.

 static int[][] matrix = null;
    int[] pointColor = null;
    int len = 0;
    int numPoint = 0;
    int numEdge = 0;
    int ret = 0;
    int numColor = 0;

    public int func(int numPoint, int numEdge, int numColor) {
        len = numPoint;
        this.numColor = numColor;
        this.numEdge = numEdge;
        this.numPoint = numPoint;
//        this.matrix = new int[numPoint + 1][numPoint + 1];
        pointColor = new int[numPoint + 1];
        backtracing(1, 1);
        return ret;
    }

    private boolean uniqueColor(int index) {
        for (int i = 1; i <= numPoint; i++) {
            if (matrix[index][i] == 1 && pointColor[index] == pointColor[i]) {
                return false;
            }
        }
        return true;
    }

    private void backtracing(int index, int cnt) {
        if (cnt >= numPoint + 1) {
            return;
        }
        for (int j = 1; j <= numColor; j++) {
            pointColor[index] = j;
            if (uniqueColor(index)) {
                if (cnt == numPoint) {
                    ret++;
                }
                for (int k = 1; k <= numPoint; k++) {
                    if (matrix[index][k] == 1 && pointColor[k] == 0) {
                        backtracing(k, cnt + 1);
                    }
                }
            }
            pointColor[index] = 0;
        }
    }
```

####

#### 100亿数字排序,求中位数,平均数,求和

##### 方法1

今天要给100亿个数字排序，100亿个 int 型数字放在文件里面大概有 37.2GB，非常大，内存一次装不下了。那么肯定是要拆分成小的文件一个一个来处理，最终在合并成一个排好序的大文件。

实现思路

1.把这个37GB的大文件，用哈希分成1000个小文件，每个小文件平均38MB左右（理想情况），把100亿个数字对1000取模，模出来的结果在0到999之间，每个结果对应一个文件，所以我这里取的哈希函数是 h = x % 1000，哈希函数取得"好"，能使冲突减小，结果分布均匀。

2.拆分完了之后，得到一些几十MB的小文件，那么就可以放进内存里排序了，可以用快速排序，归并排序，堆排序等等。

3.1000个小文件内部排好序之后，就要把这些内部有序的小文件，合并成一个大的文件，可以用**二叉堆**来做1000路合并的操作，每个小文件是一路，合并后的大文件仍然有序。

- 首先遍历1000个文件，每个文件里面取第一个数字，组成 (数字, 文件号) 这样的组合加入到堆里（假设是从小到大排序，用小顶堆），遍历完后堆里有1000个 (数字，文件号) 这样的元素
- 然后不断从堆顶拿元素出来，每拿出一个元素，把它的文件号读取出来，然后去对应的文件里，加一个元素进入堆，直到那个文件被读取完。拿出来的元素当然追加到最终结果的文件里。
- 按照上面的操作，直到堆被取空了，此时最终结果文件里的全部数字就是有序的了。

最后我用c++写了个实验程序，具体代码在[这里](https://link.jianshu.com?t=https://github.com/hehe520/Data-structure-and-algorithm/blob/master/海量数据处理/外部归并排序 - 分治.cpp)可以看到。

##### 方法2:

*1个int=4Byte   1亿个数字int = 4亿B/1024/1024=370MB*

位图是一个bit数组,遵循数组的最大长度,length的类型是int,所以最长是Integer.value.

*为什么使用位图？答：直接从1亿数据里面找Top100效率很低，通过构建位图，然后倒序输出位图就可以快速找到Top100.或者TopN*

1. 找最大值:14ms
2. 构建位图:149ms
3. 倒序输出:84m
4. 累计耗时:234ms

##### 思维拓展

类似的100亿个数字求和，求中位数，求平均数，套路就是一样的了。
 求和：统计每个小文件的和，返回给master再求和就可以了。
 求平均数：上面能求和了，再除以100亿就是平均数了
 求中位数：在排序的基础上，遍历到中间的那个数就是中位数了

#### [113. 路径总和 II](https://leetcode-cn.com/problems/path-sum-ii/)

```java
List<List<Integer>> ret = new ArrayList<>();

    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        preOrder(root, sum, 0, new ArrayList<Integer>());
        return ret;
    }

    public void preOrder(TreeNode root, int sum, int pathSum, List<Integer> path) {
        if (root == null) {
            return;
        }
        path.add(root.val);
        pathSum += root.val;
        if (root.left == null && root.right == null && pathSum == sum) {
            ret.add(new ArrayList<Integer>(path));
            path.remove(path.size() - 1);
            return;
        }
        preOrder(root.left, sum, pathSum, path);
        preOrder(root.right, sum, pathSum, path);
        path.remove(path.size() - 1);
    }
```

#### [@@@547. 朋友圈](https://leetcode-cn.com/problems/friend-circles/)

@count,parent[],size[]

@union ,getParent;

@union的时候往根节点上union

```
if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
            count--;
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
            count--;
        }
```

代码:

```java

public class Solution {
    public int findCircleNum(int[][] M) {
        if (M == null || M.length == 0) {
            return 0;
        }
        int len = M.length;
        UF u = new UF(len);
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len; j++) {
                if (M[i][j] == 1) {
                    u.union(i, j);
                }
            }
        }
        return u.getCount();
    }
}

class UF {
    private int count;
    private int[] parent;
    private int[] size;

    public UF(int len) {
        this.count = len;
        this.parent = new int[len];
        this.size = new int[len];
        for (int i = 0; i < len; i++) {
            size[i] = 1;
            parent[i] = i;
        }
    }
    public void union(int p, int q) {
        int rootP = findParent(p);
        int rootQ = findParent(q);
        if (rootP == rootQ)
            return;
        if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
            count--;
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
            count--;
        }
    }
    //路径压缩+寻找根节点
    public int findParent(int p) {
        while (parent[p] != p) {
            //把 p的爷爷 替换成  p的爸爸
            parent[p] = parent[parent[p]];
            //把p变成p的爷爷
            p = parent[p];
        }
        return p;
    }

    public int getCount() {
        return count;
    }
     public boolean connected(int p, int q) {
        int rootP = findParent(p);
        int rootQ = findParent(q);
        return rootP == rootQ;
    }

}
```

#### [@@@200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/)

@uf的初始化

i*col + j

如果`gird[i][j]=='0'` 那`parent[i*col][j]=0 和 size[i*col][j]=0`

@小岛的合并

如果`grid[i][j]=='1',先设为'0',然后跟右方和下方的岛合并`

@@查并集做法

```java
class Solution {
    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) {
            return 0;
        }
        int row = grid.length;
        int col = grid[0].length;
        UF uf = new UF(grid, grid.length, grid[0].length);
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                //如果某个坐标是一座岛,就把这座岛与右边或者下边的连接岛 合并
                //为什么没有左和上的岛,i,j是按照从左上往右下的顺序移动
                // 合并前会把grid[i][j]置为0,所以左上的岛永远是空的
                if (grid[i][j] == '1') {
                    //不置零也行   grid[i][j] = '0';
                    if (j + 1 < col && grid[i][j + 1] == '1') {
                        uf.union(i * col + j, i * col + j + 1);
                    }
                    if (i + 1 < row && grid[i + 1][j] == '1') {
                        uf.union(i * col + j, (i + 1) * col + j);
                    }
                }
            }
        }
        return uf.getCount();
    }
}

class UF {
    int count;
    int[] parent;
    int[] size;

    public UF(char[][] grid, int row, int col) {
        this.parent = new int[row * col];
        this.size = new int[row * col];
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                //如果某个坐标是导,就count++, parent初始化成自己,size初始化为1.
                if (grid[i][j] == '1') {
                    parent[i * col + j] = i * col + j;
                    ++count;
                    size[i * col + j] = 1;
                }
            }
        }
    }

    public int getCount() {
        return this.count;
    }

    public boolean connected(int p, int q) {
        int rootP = findParent(p);
        int rootQ = findParent(q);
        if (rootP == rootQ) {
            return true;
        }
        return false;
    }

    public int findParent(int p) {
        while (p != parent[p]) {
            parent[p] = parent[parent[p]];
            p = parent[p];
        }
        return p;
    }

    public void union(int p, int q) {
        int rootP = findParent(p);
        int rootQ = findParent(q);
        if (rootQ == rootP) {
            return;
        }
        if (size[rootP] > size[rootQ]) {
            size[rootP] += size[rootQ];
            parent[rootQ] = rootP;
        } else {
            size[rootQ] += size[rootP];
            parent[rootP] = rootQ;
        }
        count--;
    }

}
```

#### [@@@1319. 连通网络的操作次数](https://leetcode-cn.com/problems/number-of-operations-to-make-network-connected/)

创建查并集

如果一根网线两端已经连在一起,则空闲网线加一

如果没有连在一起,则相连,空闲网线数量不变

最后连通分量 num  和 空闲网线数作比较

```java

class Solution {
    //count密封
    public int makeConnected(int n, int[][] connections) {
        Union u = new Union(n);
        int remainLink = 0;

        for (int i = 0; i < connections.length; i++) {
            int from = connections[i][0];
            int to = connections[i][1];
            if (u.isConnected(from, to)) {
                remainLink++;
            }else{
                u.union(from, to);
            }
        }
        int groupCount = u.count;
        if (groupCount - 1 <= remainLink) {
            return groupCount - 1;
        }
        return -1;
    }

}

class Union {
    int count = 0;
    int[] size = null;
    int[] parent = null;

    public Union(int count) {
        this.count = count;
        size = new int[count];
        parent = new int[count];
        for (int i = 0; i < count; i++) {
            size[i] = 1;
            parent[i] = i;
        }
    }

    public int getParent(int x) {
        while (x != parent[x]) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    public boolean isConnected(int x, int y) {
        int xP = getParent(x);
        int yP = getParent(y);
        return xP == yP;
    }

    public void union(int x, int y) {
        if (isConnected(x, y)) {
            return;
        }
        int xP = getParent(x);
        int yP = getParent(y);
        if (size[xP] >= size[yP]) {
            parent[yP] = xP;
            size[xP] += size[yP];
        } else {
            parent[xP] = yP;
            size[yP] += size[xP];
        }
        count--;
    }
}
```

#### 找到二叉搜索树中与target之差的绝对值最小的node

@如果node处的值小于target,则node左边的值跟target的差距只会越来越大

比如target是10,如果node=9,则node左边的值,跟10的差距只会越来越大

<img src="pictures/LeetCode03/image-20200303225244927.png" alt="image-20200303225244927" style="zoom:50%;" />

在二叉查找树中，[查找与目标值最接近的节点并返回](https://www.geeksforgeeks.org/find-closest-element-binary-search-tree/)。如果有多个节点都符合要求，返回其中一个即可

```java
 private int minDiff = Integer.MAX_VALUE;
    public void maxDiffUtil(TreeNode root, int target) {
        if (root == null) {
            return;
        }
        if (root.val == target) {
            minDiff = 0;
            return;
        }
        int abs = Math.abs(root.val - target);

        minDiff = Math.min(minDiff, abs);
        if (target > root.val) {
            maxDiffUtil(root.right, target);
        } else {
            maxDiffUtil(root.left, target);
        }
    }
```

#### @@@@二叉树的非递归后序遍历

```java
private void lastOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode lastVisited = null;
        TreeNode nodeCur = root;
        Stack<TreeNode> stack = new Stack<>();
        while (nodeCur != null) {
            stack.add(nodeCur);
            nodeCur = nodeCur.left;
        }
        while (!stack.isEmpty()) {
            nodeCur = stack.pop();
            if (nodeCur.right == null || lastVisited == nodeCur.right) {
                System.out.println(nodeCur.val);
                lastVisited = nodeCur;
            } else if (lastVisited == nodeCur.left) {
                stack.add(nodeCur);
                nodeCur = nodeCur.right;
                while (nodeCur != null) {
                    stack.add(nodeCur);
                    nodeCur = nodeCur.left;
                }
            }
        }
    }
```

#### @@@有序数组重建平衡二叉树

```java
public TreeNode rebuildBST(int[] nums, int start, int end) {
        if (start > end || start < 0 || end > nums.length - 1) {
            return null;
        }
        int mid = start + (end - start) / 2;
        TreeNode treeNode = new TreeNode(nums[mid]);
        treeNode.left = rebuildBST(nums, start, mid - 1);
        treeNode.right = rebuildBST(nums, mid + 1, end);
        return treeNode;
    }
```

#### 一个数组里找任意两个数之和的绝对值最小值

```
排序,left= 0,right = len-1, sum>0 right--,sum<0,left++.
```

#### 一个整数数组找任意两数的最小差

```
排序,相邻的数的差最小
```

#### [@@208. 实现 Trie (前缀树)](https://leetcode-cn.com/problems/implement-trie-prefix-tree/)

```java

class Trie {
    private TreeNode root = null;

    /**
     * Initialize your data structure here.
     */
    public Trie() {
        root = new TreeNode();
    }

    public TreeNode getPrefixEnd(String word) {
        TreeNode node = root;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            if (node.containsKey(ch)) {
                node = node.get(ch);
            } else {
                return null;
            }
        }
        return node;
    }

    /**
     * Inserts a word into the trie.
     */
    public void insert(String word) {
        TreeNode node = root;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            if (!node.containsKey(ch)) {
                node.put(ch, new TreeNode());
            }
            node = node.get(ch);
        }
        node.setEnd();
    }

    /**
     * Returns if the word is in the trie.
     */
    public boolean search(String word) {
        TreeNode node = getPrefixEnd(word);
        return node != null && node.isEnd();
    }

    /**
     * Returns if there is any word in the trie that starts with the given prefix.
     */
    public boolean startsWith(String prefix) {
        TreeNode node = getPrefixEnd(prefix);
        return node != null;
    }
}

class TreeNode {
    private int len = 26;
    private TreeNode[] links = null;
    private boolean isEnd = false;

    public TreeNode() {
        links = new TreeNode[len];
    }

    public void put(char ch, TreeNode node) {
        links[ch - 'a'] = node;
    }

    public TreeNode get(char ch) {
        return links[ch - 'a'];
    }

    public boolean containsKey(char ch) {
        return links[ch - 'a'] != null;
    }

    public boolean isEnd() {
        return isEnd;
    }

    public void setEnd() {
        isEnd = true;
    }

}
```

#### [@209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/)

思路一:滑动窗口  复杂度 O(n)

@@@思路二:二分长度法   len = 8 先搜len=4,如果sum>s,搜len=2,如果sum<s,搜len=6.  在长度上用二分法

#### [@@60. 第k个排列](https://leetcode-cn.com/problems/permutation-sequence/)

@@特别注意使用

```java
 public String getPermutation(int n, int k) {
        int[] factor = new int[n];
        StringBuilder ret = new StringBuilder();
        ArrayList<Integer> numList = new ArrayList<Integer>();
        //k以0为初始
        k = k - 1;
        //n=5,k=11
        //factor为[0,1,2,0,0]
        //位次是 0*4!+ 1*3!+2*2!+0*1!+0*0! = 10.
        //  4   3   2   1   0
        //  4!  3!  2!  1!  0!
     //从前往后构造factor,即每一位的顺序
        for (int i = 0, j = n - 1; i < n; i++, j--) {
            int weight = weight(j);
            factor[i] = k / weight;
            k = k % weight;
        }
        for (int i = 1; i <= n; i++) {
            numList.add(i);
        }
        //从前往后构造排列
        for (int i = 0; i < n; i++) {
            int num = numList.get(factor[i]);
            ret.append(num);
            numList.remove(factor[i]);
        }
        return ret.toString();
    }
    //返回n的阶乘
    private int weight(int n) {
        int sum = 1;
        while (n >= 1) {
            sum = sum * n;
            n--;
        }
        return sum;
    }
```

#### [@@@@162. 寻找峰值](https://leetcode-cn.com/problems/find-peak-element/)

迭代法:

@@@mid和mid+1的比较是很有水平的.因为while里面是left<right.所以mid求出来之后,mid+1不会越界,但mid-1会越界

所以可以

```java
 if (nums[mid] > nums[mid + 1]) {
            right = mid;
        } else if (nums[mid] <= nums[mid + 1]) {
            left = mid + 1;
        }
```

但是不能用

```java
if(nums[mid-1]<nums[mid]){
    left = mid;
}else if(nums[mid-1]>=nums[mid]){
    right = mid-1;
}
```

```java
public int findPeakElement(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[mid + 1]) {
            right = mid;
        } else if (nums[mid] <= nums[mid + 1]) {
            left = mid + 1;
        }
    }
    return left;
}
```

递归法:边界的判断.

```java
 public int findPeakElement(int[] nums) {
        return func(nums, 0, nums.length - 1);
    }

    private int func(int[] nums, int left, int right) {
        if (left == right) {
            return left;
        }
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[mid + 1]) {
            right = mid;
            return func(nums, left, right);
        } else {
            left = mid + 1;
            return func(nums, left, right);
        }
    }
```

#### [@@@@127. 单词接龙](https://leetcode-cn.com/problems/word-ladder/)

这个题不能用dfs来做,假设dict是100.那么dfs要遍历所有的路径,复杂度是100的100次方,大概就是1*10的200次方,复杂度是指数级别的

用BFS来做,就可以找到最少的转换次数,因为有level层来控制.复杂度变成了  dict表长*字符串的len

```java
   boolean[] visited = null;
    String target = null;
    List<String> wordList = null;

    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        target = endWord;
        this.wordList = wordList;
        visited = new boolean[wordList.size()];

        if (!wordList.contains(endWord)) {
            return 0;
        }

        for (int i = 0; i < wordList.size(); i++) {
            if (beginWord.equals(wordList.get(i))) {
                visited[i] = true;
            }
        }

        LinkedList<Pair<String, Integer>> queue = new LinkedList<>();
        queue.add(new Pair(beginWord, 1));
        while (!queue.isEmpty()) {
            //每一层
            for (int queueIndex = 0; queueIndex < queue.size(); queueIndex++) {
                String curStr = queue.peek().getKey();
                int level = queue.poll().getValue();
                //每一层的某个string,能否往下转换
                for (int i = 0; i < wordList.size(); i++) {
                    String nextStr = wordList.get(i);
                    if (visited[i] == false && canTrans(curStr, nextStr)) {
                        if (nextStr.equals(endWord)) {
                            return level + 1;
                        }
                        queue.add(new Pair<>(nextStr, level + 1));
                        visited[i] = true;
                    }
                }
            }

        }

        return 0;
    }

    private boolean canTrans(String curStr, String nextStr) {
        int diffCount = 0;
        if (curStr.equals(nextStr)) {
            return false;
        }
        for (int i = 0; i < curStr.length(); i++) {
            if (curStr.charAt(i) != nextStr.charAt(i)) {
                diffCount++;
            }
            if (diffCount > 1) {
                return false;
            }
        }
        return diffCount == 1;
    }
```

#### IP重复攻击

实现一个数据结构，判断某个IP是否在1秒内请求了>=100次，用来在服务器上防止拒绝服务攻击

```
用循环数组，只需要长度为100，后来的请求覆盖掉之前的请求，然后判断最新的一个请求和队列中最早的请求时间差是否>=1。
```

#### ab替换成c,b替换成ef

输入一个char\[\] str，要求把str中所有的"ab"都替换成"c"，把所有单个的"b"都替换成"ef"。要求in-place，并且保证str中"ab"的个数 >"b"的个数（也就是str的长度足够放下替换后的结果）

```java
//ab替换成c
//b替换成ef

private String handleStr(char[] chars) {
    int fillIndex = 0;
    int countB = 0;
    for (int i = 0; i < chars.length; ) {
        char ch = chars[i];
        if (ch == 'a') {
            if (i + 1 < chars.length && chars[i + 1] == 'b') {
                i++;
                chars[fillIndex++] = 'c';
            }
            i++;
        } else {
            if (ch == 'b') {
                countB++;
            }
            chars[fillIndex++] = chars[i++];
        }
    }
    for (int j = fillIndex; j < chars.length; j++) {
        chars[j] = 0;

    }
    int rightIndex = fillIndex + countB - 1;
    for (int i = fillIndex - 1; i >= 0; ) {
        char ch = chars[i];
        if (ch == 'b') {
            chars[rightIndex--] = 'f';
            chars[rightIndex--] = 'e';
            i--;
        } else {
            chars[rightIndex--] = chars[i--];
        }
    }
    return new String(chars);
}
```

#### [@@@4. 寻找两个有序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/)

@@@各种边界 非常麻烦

```java
思路1:log(m+n)的算法
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int len1 = nums1.length;
    int len2 = nums2.length;
    int sum = len1 + len2;
    if (sum % 2 == 1) {
        return findKth(nums1, 0, len1 - 1, nums2, 0, len2 - 1, sum / 2 + 1);
    } else {
        return (findKth(nums1, 0, len1 - 1, nums2, 0, len2 - 1, sum / 2) + findKth(nums1, 0, len1 - 1, nums2, 0, len2 - 1, sum / 2 + 1)) * 0.5;
    }
}

public int findKth(int[] nums1, int left1, int right1, int[] nums2, int left2, int right2, int k) {
    int len1 = right1 - left1 + 1;
    int len2 = right2 - left2 + 1;
    //让len1始终保持最小
    if (len1 > len2) {
        return findKth(nums2, left2, right2, nums1, left1, right1, k);
    }
    if (len1 == 0) {
        return nums2[left2 + k - 1];
    }
    if (k == 1) {
        return Math.min(nums1[left1], nums2[left2]);
    }
    //找到left 加 k/2长度处的两个数组的值进行比较
    int index1 = left1 + Math.min(len1, k / 2) - 1;
    int index2 = left2 + Math.min(len2, k / 2) - 1;
    if (nums1[index1] < nums2[index2]) {
        return findKth(nums1, index1 + 1, right1, nums2, left2, right2, k - (index1 - left1 + 1));
    } else {
        return findKth(nums1, left1, right1, nums2, index2 + 1, right2, k - (index2 - left2 + 1));
    }
}

思路2:往优先队列里面添加元组,直到添加(m+n)/2个
```

#### [@@@@@701. 二叉搜索树中的插入操作](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/)

@插入操作的思想:

找到null,就创建并返回

修改左子树或者右子树,就通过返回值来做更新

最后把整个root返回回去,更新root的root.

```java
递归写法:

public TreeNode insertIntoBST(TreeNode root, int val) {
    if (root == null) {
        return new TreeNode(val);
    }
    if(root.val==val){
            return root;
    }else if (root.val < val) {
        root.right = insertIntoBST(root.right, val);
    } else {
        root.left = insertIntoBST(root.left, val);
    }
    return root;
}

迭代写法:

 public TreeNode insertIntoBST(TreeNode root, int val) {
        if (root == null) {
            return new TreeNode(val);
        }
        TreeNode node = root;
        while (node != null) {
            if (node.val == val) {
                return root;
            } else if (node.val > val) {
                if (node.left == null) {
                    node.left = new TreeNode(val);
                    return root;
                }
                node = node.left;
            } else {
                if (node.right == null) {
                    node.right = new TreeNode(val);
                    return root;
                }
                node = node.right;
            }
        }

        return root;
    }
```

#### [@@@@@@@@@@450. 删除二叉搜索树中的节点](https://leetcode-cn.com/problems/delete-node-in-a-bst/)

@@@最难的部分 (换了一下两个if else的顺序,变得好理解多了)

```java
private TreeNode delLeftMaxChild(TreeNode node) {
    if( node.right !=null){
         node.right = delLeftMaxChild(node.right);
   		 return node;
    }else if (node.right == null) {
        TreeNode left = node.left;
        node.left = null;
        return left;
    }
}
```

```java
public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) {
            return null;
        }
        if (root.val == key) {
            root = getDeletedLeftMaxChildTree(root, key);
        } else if (root.val > key) {
            root.left = deleteNode(root.left, key);
        } else {
            root.right = deleteNode(root.right, key);
        }
        return root;
    }

    private TreeNode getDeletedLeftMaxChildTree(TreeNode root) {
        if (root.left == null && root.right == null) {
            return null;
        } else if (root.left != null && root.right == null) {
            TreeNode left = root.left;
            root.left = null;
            return left;
        } else if (root.left == null && root.right != null) {
            TreeNode right = root.right;
            root.right = null;
            return right;
        } else {
            TreeNode leftMaxChild = findMaxLeftChild(root);
            TreeNode copyRootTree = new TreeNode(leftMaxChild.val);
            copyRootTree.left = delLeftMaxTree(root.left);
            copyRootTree.right = root.right;
            return copyRootTree;
        }
    }

    private TreeNode delLeftMaxTree(TreeNode root) {
        if (root.right != null) {
            root.right = delLeftMaxTree(root.right);
            return root;
        } else {
            TreeNode left = root.left;
            root.left = null;
            return left;
        }
    }

 	//仅用于左右子树都存在的情况
    private TreeNode findMaxChild(TreeNode root){

        TreeNode left = root.left;
        while(left.right!=null){
            left = left.right;
        }
        return left;
    }
```

#### [654. 最大二叉树](https://leetcode-cn.com/problems/maximum-binary-tree/)

没办法用排序来做,本题不能破坏原来的左右子树的关系.如果可以破坏该关系的话,则

```java
int[] nums = null;

    public TreeNode constructMaximumBinaryTree(int[] nums) {
        this.nums = nums;
        Arrays.sort(nums);
        TreeNode root = buildTree();
        return root;
    }

    private TreeNode buildTree(int start, int end) {
        if (start > end) {
            return null;
        }
        TreeNode root = new TreeNode(nums[start]);
        int mid = start + (end - start) / 2;
        root.left = buildTree(...);
        root.right = buildTree(..);
        return root;
    }
```

@inOrder  +   数组递归建树的诀窍: left,maxIndex-1   和  maxIndex+1,right

```java
public TreeNode constructMaximumBinaryTree(int[] nums) {
    if (nums == null || nums.length == 0) {
        return null;
    }
    return construct(nums, 0, nums.length - 1);
}

private TreeNode construct(int[] nums, int left, int right) {
    if (left > right) {
        return null;
    }
    int[] ret = findMax(nums, left, right);
    TreeNode node = new TreeNode(ret[0]);
    node.left = construct(nums, left, ret[1] - 1);
    node.right = construct(nums, ret[1] + 1, right);
    return node;
}

private int[] findMax(int[] nums, int left, int right) {
    if (left > right) {
        return new int[]{};
    }
    int max = Integer.MIN_VALUE;
    int maxIndex = -1;
    for (int i = left; i <= right; i++) {
        if (nums[i] > max) {
            max = nums[i];
            maxIndex = i;
        }
    }
    return new int[]{max, maxIndex};
}
```

#### [@@@273. 整数转换英文表示](https://leetcode-cn.com/problems/integer-to-english-words/)

@one里面没有0的返回

```java
class Solution {
    public String numberToWords(int num) {
        if (num == 0) {
            return "Zero";
        }
        int billion = num / 1000000000;
        int million = (num % 1000000000) / 1000000;
        int thousand = (num % 1000000) / 1000;
        int rest = num % 1000;

        String ret = "";
        if (billion != 0) {
            ret = three(billion) + " " + "Billion";
        }
        if (million != 0) {
            if (!ret.isEmpty()) {
                ret += " ";
            }
            ret += three(million) + " " + "Million";
        }
        if (thousand != 0) {
            if (!ret.isEmpty()) {
                ret += " ";
            }
            ret += three(thousand) + " " + "Thousand";
        }
        if (rest != 0) {
            if (!ret.isEmpty()) {
                ret += " ";
            }
            ret += three(rest);
        }
        return ret;
    }

    public String one(int num) {
        switch (num) {
            case 1:
                return "One";
            case 2:
                return "Two";
            case 3:
                return "Three";
            case 4:
                return "Four";
            case 5:
                return "Five";
            case 6:
                return "Six";
            case 7:
                return "Seven";
            case 8:
                return "Eight";
            case 9:
                return "Nine";
        }
        return "";
    }

    public String twoLessThan20(int num) {
        switch (num) {
            case 10:
                return "Ten";
            case 11:
                return "Eleven";
            case 12:
                return "Twelve";
            case 13:
                return "Thirteen";
            case 14:
                return "Fourteen";
            case 15:
                return "Fifteen";
            case 16:
                return "Sixteen";
            case 17:
                return "Seventeen";
            case 18:
                return "Eighteen";
            case 19:
                return "Nineteen";
        }
        return "";
    }

    public String ten(int num) {
        switch (num) {
            case 2:
                return "Twenty";
            case 3:
                return "Thirty";
            case 4:
                return "Forty";
            case 5:
                return "Fifty";
            case 6:
                return "Sixty";
            case 7:
                return "Seventy";
            case 8:
                return "Eighty";
            case 9:
                return "Ninety";
        }
        return "";
    }

    public String two(int num) {
        if (num == 0) {
            return "";
        }
        int two = num / 10;
        int rest = num % 10;
        if (num < 10) {
            return one(num);
        } else if (num < 20) {
            return twoLessThan20(num);
        } else {
            if (two != 0 && rest != 0) {
                return ten(two) + " " + one(rest);
            } else if (two != 0 && rest == 0) {
                return ten(two);
            }
        }
        return "";
    }

    public String three(int num) {
        if (num == 0) {
            return "";
        }
        int hundred = num / 100;
        int rest = num % 100;
        String ret = "";
        if (rest != 0 && hundred != 0) {
            return one(hundred) + " " + "Hundred" + " " + two(rest);
        } else if (hundred != 0) {
            return one(hundred) + " " + "Hundred";
        } else if (rest != 0) {
            return two(rest);
        }
        return "";
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        String s = solution.numberToWords(10);
        System.out.println(s);
    }
}
```

#### [272. 最接近的二叉搜索树值 II](https://leetcode-cn.com/problems/closest-binary-search-tree-value-ii/)

```java
private PriorityQueue<Pair<Double, Integer>> queue = null;
int k = 0;
public List<Integer> closestKValues(TreeNode root, double target, int k) {
    Comparator<Pair<Double, Integer>> c = new Comparator<Pair<Double, Integer>>() {
        public int compare(Pair<Double, Integer> p1, Pair<Double, Integer> p2) {
            if ((p1.getKey() - p2.getKey()) < 0) {
                return 1;
            }
            return -1;
        }
    };
    queue = new PriorityQueue<Pair<Double, Integer>>(10, c);
    this.k = k;
    inOrder(root, target);
    ArrayList<Integer> ret = new ArrayList<Integer>();
    while (!queue.isEmpty()) {
        ret.add(queue.poll().getValue());
    }
    return ret;
}

public void inOrder(TreeNode root, double target) {

    if (root == null) {
        return;
    }
    inOrder(root.left, target);
    double diff = Math.abs((double) root.val - target);
    if (queue.size() < k) {
        queue.add(new Pair(diff, root.val));
    } else {
        if (queue.peek().getKey() > diff) {
            queue.poll();
            queue.add(new Pair(diff, root.val));
        }
    }
    inOrder(root.right, target);
}
```

#### @@@**拓扑排序**

拓扑排序有dfs和bfs(kahn)两种.

bfs可以判断有环无环

dfs麻烦一点

#### [@@210. 课程表 II](https://leetcode-cn.com/problems/course-schedule-ii/)

@@@

<img src="pictures/LeetCode03/image-20200324175541265.png" alt="image-20200324175541265" style="zoom:50%;" />

@思路1 拓扑排序(BFS)
建立一个**入度表**  和 一个**邻接矩阵**.

如果不涉及权值,为了节省空间和提升遍历效率,**邻接矩阵可以用 set数组代替,HashSet<Integer>[]**

```java

int[] inCount = null;
int[][] grid = null;
int[] ret = null;
ArrayList<Integer> retList = null;
Queue<Integer> queue = new LinkedList<Integer>();

public int[] findOrder(int numCourses, int[][] prerequisites) {
    if (numCourses <= 0) {
        return new int[]{};
    }
    int len = numCourses;
    inCount = new int[len];
    grid = new int[len][len];
    retList = new ArrayList<>();
    ret = new int[len];

    for (int i = 0; i < prerequisites.length; i++) {
        int to = prerequisites[i][0];
        int from = prerequisites[i][1];
        grid[from][to] = 1;
        inCount[to]++;
    }

    for (int i = 0; i < len; i++) {
        if (inCount[i] == 0) {
            queue.add(i);
            retList.add(i);
        }
    }
    while (!queue.isEmpty()) {
        Integer node = queue.poll();
        for (int i = 0; i < len; i++) {
            if (grid[node][i] == 1) {
                grid[node][i] = 0;
                inCount[i]--;
                if (inCount[i] == 0) {
                    queue.add(i);
                    retList.add(i);
                }
            }
        }
    }
    System.out.println(retList);
    //如果节点没有被全部遍历,则代表有环
    if (retList.size() == len) {
        for (int i = 0; i < len; i++) {
            ret[i] = retList.get(i);
        }
        return ret;
    }
    return new int[]{};
}
```

#### [@207. 课程表](https://leetcode-cn.com/problems/course-schedule/)

@思路1

BFS

@思路2  DFS  这不是排序,而是用flags的0,-1,1代表三种状态,来判断是否有环,如果没有环的话,那一定能满足拓扑排序.

```java
private int[] flags = null;
private int[][] grid = null;
int len = 0;

public boolean canFinish(int numCourses, int[][] prerequisites) {
    this.len = numCourses;
    grid = new int[len][len];
    flags = new int[len];
    for (int i = 0; i < prerequisites.length; i++) {
        int to = prerequisites[i][0];
        int from = prerequisites[i][1];
        grid[from][to] = 1;
    }
    for (int i = 0; i < len; i++) {
        if (!dfs(grid, flags, i)) {
            return false;
        }
    }
    return true;
}

private boolean dfs(int[][] grid, int[] flags, int start) {
    if (flags[start] == 1) {
        return false;
    }
    if (flags[start] == -1) {
        return true;
    }
    flags[start] = 1;
    for (int i = 0; i < len; i++) {
        if (grid[start][i] == 1) {
            if (!dfs(grid, flags, i)) {
                return false;
            }
        }
    }
    flags[start] = -1;
    return true;
}
```

#### [@leetcode.298 二叉树的最长连续子序列](https://blog.csdn.net/jmspan/article/details/51171217)

```
@@@@这个题也许可以用动规的思路来做,对于二叉树 2 4 3 5 6 7 8
dp[root] = dp[left] or dp[right]  + 1   ;   if root.val = left.val-1 || right.val-1

如果4的左右子树里有节点值为 4+1 = 5,那么node.val为4的树的长度序列为 该子树序列长度+1.
这个过程可以被记录进memo
```

```java
int maxLen = 0;

public int longestConsecutive(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int len = 1;
    maxLen = Math.max(len, maxLen);
    TreeNode left = root.left;
    TreeNode right = root.right;
    if (left != null) {
        inOrder(left, len, root.val);
    }
    if (right != null) {
        inOrder(right, len, root.val);
    }
    return maxLen;
}

private void inOrder(TreeNode root, int len, int preVal) {
    if (root == null) {
        return;
    }
    if (root.val != preVal + 1) {
        len = 1;
    } else {
        len = len + 1;
    }
    maxLen = Math.max(len, maxLen);
    TreeNode left = root.left;
    TreeNode right = root.right;
    if (left != null) {
        inOrder(left, len, root.val);
    }
    if (right != null) {
        inOrder(right, len, root.val);
    }
}

public static void main(String[] args) {
    Solution solution = new Solution();
    TreeNode t1 = new TreeNode(1);
    TreeNode t2 = new TreeNode(-1);
    TreeNode t3 = new TreeNode(2);
    TreeNode t4 = new TreeNode(1);
    TreeNode t5 = new TreeNode(4);
    TreeNode t6 = new TreeNode(5);
    t1.right = t2;
    t2.left = t3;
    t3.left = t4;
    t2.right = t5;
    t5.right = t6;
    int i = solution.longestConsecutive(t1);
    System.out.println(i);

}
```

#### [@@面试题51. 数组中的逆序对](https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/)

```
@@@逆序对的个数 在 nums[leftIndex]>nums[rightIndex]的时候出发
count+=mid-leftIndex+1;意思是,如果左半边的数 5  比右半边的 4  大,则5右边的 6 7 8 都比4大,都能组成逆序对
```

```java
int count = 0;

public int reversePairs(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    mergeSort(nums, 0, nums.length - 1);
    return count;
}

private void mergeSort(int[] nums, int left, int right) {
    if (left >= right) {
        return;
    }
    int mid = left + (right - left) / 2;
    mergeSort(nums, left, mid);
    mergeSort(nums, mid + 1, right);
    merge(nums, left, right);
}

private void merge(int[] nums, int left, int right) {
    if (left >= right) {
        return;
    }
    int mid = left + (right - left) / 2;
    int len = right - left + 1;
    int index = 0;
    int[] tempNums = new int[len];
    int leftIndex = left;
    int rightIndex = mid + 1;
    while (leftIndex <= mid && rightIndex <= right) {
        if (nums[leftIndex] <= nums[rightIndex]) {
            tempNums[index++] = nums[leftIndex++];
        } else {
            count += mid - leftIndex + 1;
            tempNums[index++] = nums[rightIndex++];
        }
    }
    while (leftIndex <= mid) {
        tempNums[index++] = nums[leftIndex++];
    }
    while (rightIndex <= right) {
        tempNums[index++] = nums[rightIndex++];
    }
    for (int i = 0, j = left; i < len; j++, i++) {
        nums[j] = tempNums[i];
    }
    return;
}
```

#### [@@63. 不同路径 II](https://leetcode-cn.com/problems/unique-paths-ii/)

```
@@思路1.带cache的dfs.需要再写几遍熟练

@@思路2.dp[i][j]代表到达这个位置有多少可能,取决于dp[i-1][j]+dp[i][j-1]  自底向上
@@@@@@@当时还用了另一种搞笑的自底向上: dp[rowCount-1][colCount-1]定为了1,这种想法是错误的!!!!!!
```

```java
//1. cache初始化为-1. 因为cache在等于0的时候依然可能是缓存数据
//2. 不需要visited[][],因为只往右或者往下走
int rowCount = 0;
int colCount = 0;
int[][] loc = new int[][]{{1, 0}, {0, 1}};
int[][] cache = null;
int[][] matrix = null;

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    if (obstacleGrid == null) {
        return 0;
    }
    matrix = obstacleGrid;
    if (matrix[0][0] == 1) {
        return 0;
    }
    rowCount = matrix.length;
    colCount = matrix[0].length;
    cache = new int[rowCount][colCount];
    for (int i = 0; i < rowCount; i++) {
        for (int j = 0; j < colCount; j++) {
            cache[i][j] = -1;
        }
    }
    return backtracing(0, 0);
}

private int backtracing(int x, int y) {
    int retTemp = 0;
    if (x == rowCount - 1 && y == colCount - 1) {
        return 1;
    }
    if (cache[x][y] != -1) {
        return cache[x][y];
    }
    for (int i = 0; i < loc.length; i++) {
        int newX = x + loc[i][0];
        int newY = y + loc[i][1];
        if (newX >= 0 && newY >= 0 && newX <= rowCount - 1 && newY <= colCount - 1 && matrix[newX][newY] != 1) {
            retTemp += backtracing(newX, newY);
        }
    }
    cache[x][y] = retTemp;
    return cache[x][y];
}

```

```java
int rowCount = 0;
int colCount = 0;
int[][] loc = new int[][]{{1, 0}, {0, 1}};
int[][] dp = null;
int[][] matrix = null;

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    if (obstacleGrid == null) {
        return 0;
    }
    matrix = obstacleGrid;
    if (matrix[0][0] == 1) {
        return 0;
    }
    rowCount = matrix.length;
    colCount = matrix[0].length;
    dp = new int[rowCount][colCount];
    dp[0][0] = 1;
    for (int i = 1; i < colCount; i++) {
        if (matrix[0][i] == 1) {
            dp[0][i] = 0;
        } else {
            dp[0][i] += dp[0][i - 1];
        }
    }
    for (int i = 1; i < rowCount; i++) {
        if (matrix[i][0] == 1) {
            dp[i][0] = 0;
        } else {
            dp[i][0] += dp[i - 1][0];
        }
    }
    for (int i = 1; i < rowCount; i++) {
        for (int j = 1; j < colCount; j++) {
            if (matrix[i][j] == 1) {
                dp[i][j] = 0;
            } else {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
    }
    return dp[rowCount - 1][colCount - 1];
}
```

#### [单位圆能框住的最多节点数](https://blog.csdn.net/Ramay7/article/details/51147778)

题目：
POJ 1981 Circle and Points
HDU 1077 Catching Fish
题意：
给出n个点的二维点坐标，问单位圆最多能覆盖多少点？
分析：
①：
最优的情况一定是有两个点在圆弧上。先枚举两个点，计算两点在圆弧上的单位圆（一般会有两个）
但是可以统一取一个方向的（也就是AB取一个然后BA取另外一个）.然后枚举所有点,计算在这个单位圆内的点的个数。
这样做的时间复杂度是O(n^3).
②：
对每个点以R为半径画圆，对N个圆两两求交。这一步O(N^2)。问题转化为求被覆盖次数最多的弧。
因为如果最优圆覆盖A点那么最优圆一定在以A点为圆心的圆弧上。那么圆弧倍覆盖多少次也就意味着
以这条圆弧为上任意一点为圆心花园能覆盖多少点。
对每一个圆，求其上的每段弧重叠次数。假如A圆与B圆相交。A上[PI/3, PI/2]的区间被B覆盖(PI为圆周率)。
那么对于A圆，我们在PI/3处做一个+1标记，在PI/2处做一个-1标记。
对于[PI*5/3, PI*7/3]这样横跨0点的区间只要在0点处拆成两段即可。
将一个圆上的所有标记排序，从头开始扫描。初始total=0，碰到+1标记给total++，碰到-1标记total–。
扫描过程中total的最大值就是圆上被覆盖最多的弧。求所有圆的total的最大值就是答案。
极角排序需要2*n*logn，总复杂度O(N^2 * logN)

```python
//1264K 1357MS
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <climits>
#include <cmath>
using namespace std;
const int MAX_N=310;
const double eps=1e-6;
const double R=1.0;//定义覆盖圆半径为R

int T,n;

struct Point{
    double x,y;
}point[MAX_N];

inline double dis(Point a,Point b)
{
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}

inline Point GetCenter(Point a,Point b)
{//获取a,b两点在圆周上的单元圆圆心,单位圆圆心有两个
    struct Point mid,res;
    mid.x=(a.x+b.x)/2,mid.y=(a.y+b.y)/2;//mid是a,b中点坐标
    double angle=atan2(b.y-a.y,b.x-a.x);//angle是直线ab的倾斜角
    double tmp=dis(a,b)/2;//tmp是线段ab长度的一半
    double d=sqrt(1.0-tmp*tmp);//d是ab中点到圆心的距离
    res.x=mid.x-d*sin(angle);//res是直线ab左边的那个圆心
    res.y=mid.y+d*cos(angle);
    //下面的res是直线ab右边的那个圆心
    //res.x=mid.x+d*sin(angle);
    //res.y=mid.y-d*cos(angle);
    return res;
}

int main()
{
    scanf("%d",&T);
    while(T--){
        scanf("%d",&n);
        for(int i=0;i<n;i++){
            scanf("%lf%lf",&point[i].x,&point[i].y);
        }
        int ans=1;//初始化至少能覆盖一个点！！！
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                if(i==j||dis(point[i],point[j])-2*R>eps) continue;
                int cnt=0;
                struct Point center=GetCenter(point[i],point[j]);
                for(int k=0;k<n;k++){
                    if(dis(point[k],center)-R<=eps) cnt++;
                }
                ans=max(ans,cnt);
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}
```

#### [@@@四种操作求A的最大次数](https://blog.csdn.net/qq_40147449/article/details/93376889)

```
思路
@ i为2之前  dp[i]max  = i
@ dp[i] 的状态过程有三类   一种是从j开始全选,复制 一直粘贴,二种是i-2开始打印两个,三种是i-1开始 打印一个
dp[i] = Max(dp[i-1]+1,dp[i-2]+2,dp[j]*(i-j+1)); j从2到i-2;

@@dp[i] 初始化为 i
```

```
描述
假设你有一个特殊的键盘，键盘上有如下键:

键1: (A): 在屏幕上打印一个’A’。
键2: (Ctrl-A): 选择整个屏幕。
键3: (Ctrl-C): 复制选择到缓冲区。
键4: (Ctrl-V): 在屏幕上已有的内容后面追加打印缓冲区的内容。
现在，你只能按键盘上N次(使用以上四个键)，找出你可以在屏幕上打印的“A”的最大数量

样例
1
输入: 3
输出: 3
解释: A, A, A

2
输入: 7
输出: 9
解释: A, A, A, Ctrl A, Ctrl C, Ctrl V, Ctrl V

注意事项
1 <= N <= 50
答案将在32位有符号整数的范围内。

思路
动态规划
dpi表示i个操作所能得到的最多的A的个数
初始状态下 dpi=0，当i<=6的时候容易得到dpi=i
对于任意dpi 可以省去j个操作
j=1 dpi=1+dpi-1
j=2 dpi=2+dpi-2
当j>2的时候，可以全选 复制 粘贴，所以dpi = j * dp(i-1-j)
取其中最大值即可
```

```java
public int maxA(int n) {
    if (n <= 0) {
        return 0;
    }
    int[] dp = new int[n + 1];
    for (int i = 0; i <= n; i++) {
        if (i <= 2) {
            dp[i] = i;
            continue;
        }else{
            //初始化一个状态,dp[i]处的至少有这么多个字母
            dp[i] = i;
        }
        //全选,复制,粘贴至少从第二位开始
        //最多提前两位操作   比如 i= 8  6全选,7复制,8粘贴
        //j从第二位,开始.依次计算全选,复制,粘贴,粘贴...粘贴一共 (i-j-1)次 ,
        // 粘贴量为dp[j]处A的个数
        for (int j = 2; j <= i - 2; j++) {
            dp[i] = Math.max(dp[i], (i - j - 1) * dp[j]);
        }
        int preTwoEasyAdd = Math.max(dp[i - 1] + 1, dp[i - 2] + 2);
        dp[i] = Math.max(dp[i], preTwoEasyAdd);
    }
    System.out.println(dp[n]);
    return dp[n];
}
```

#### 点集旋转

作者：Ire1ia
链接：https://www.nowcoder.com/discuss/373413?type=post&order=time&pos=&page=2
来源：牛客网

二维平面上有一个点集P, 另外点集外有一个点P0, 现在令P中的所有点绕P0旋转，求旋转的角度为多少时, P中的所有点能与原位置重合. 我的答案是将P中的点按照与P0的距离划分成一些子集, 每个子集分别枚举可能的旋转角度, 判断是否与初始位置重合, 最后各个子集的答案求一个最小公倍数即可.

比如正方形四个角为点集,中心点为P0

#### [@@面试题 16.06. 最小差](https://leetcode-cn.com/problems/smallest-difference-lcci/)

```
@binarySearch返回的值  如果是0~len-1 则找到了target.如果是负数,则负数结果为  -insertPositon-1  可以用

insetPostion = -(  -insertPositon-1 ) -1;还原.

还原之后,因为insertPostion代表第一个大于tartget的数字的位置.所以,
如果是 0 则 只需要计算b[0]-target;
如果结果是len,则只需要计算target - b[len -1].
其他1~len-1,左右两边都需要计算.

diff要判断是否大于0,是为了防止
[-2147483648,1]
[0,2147483647]
当target = -2147483648时,index = 0.
但是b[index]-target 不是整数(2147483648会被淘汰),而是负数了,负数就会被取到,就不符合题意了.
```

```java
public int smallestDifference(int[] a, int[] b) {
    if (a == null || b == null) {
        return 0;
    }
    int ret = Integer.MAX_VALUE;
    Arrays.sort(a);
    Arrays.sort(b);
    for (int i = 0; i = 0) {
            return 0;
        }
        index = -index - 1;
        if (index == 0) {
            int diff = b[index] - target;
            if (diff > 0) {
                ret = Math.min(ret, diff);
            }
        } else if (index == len) {
            int diff = target - b[index - 1];
            if (diff > 0) {
                ret = Math.min(ret, diff);
            }
        } else {
            int diff = b[index] - target;
            if (diff > 0) {
                ret = Math.min(ret, diff);
            }
            diff = target - b[index - 1];
            if (diff > 0) {
                ret = Math.min(ret, diff);
            }
        }
    }
    return ret;
}
```

#### [@670. 最大交换](https://leetcode-cn.com/problems/maximum-swap/)

```java
public int maximumSwap(int num) {
    PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10, (o1, o2) -> (o2 - o1));
    int numLen = 0;
    List<Integer> list = new ArrayList<Integer>();
    while (num != 0) {
        list.add(0, num % 10);
        queue.add(num % 10);
        num = num / 10;

    }
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i) == queue.peek()) {
            queue.poll();
        } else {
            for (int j = list.size() - 1; j > i; j--) {
                if (list.get(j) == queue.peek()) {
                    int temp = list.get(j);
                    list.set(j, list.get(i));
                    list.set(i, temp);
                    break;
                }
            }
            break;
        }
    }
    num = 0;
    for (int i = 0; i < list.size(); i++) {
        num = num * 10 + list.get(i);
    }
    return num;
}
```

#### [@@@@50. Pow(x, n)](https://leetcode-cn.com/problems/powx-n/)

@@@看注释,此题细节非常多

//优化  @@对半计算  1 和 0 和 -1 可以优化

```java
//指数:
//  -1 -2 -3
//   1  2  3

//底数
// 正  负  0

//越界 n 的 负数越界问题

//重复数字  用优先队列来解决

//优化  @@对半计算  1 和 0 和 -1 可以优化  2也可以优化左移

public double myPow(double x, int n) {
    //x 为正
    //n为负数,就一定要去 分之一
    //x为负 且n为奇数  考虑符号
    boolean isNegative = false;
    boolean oneDivid = false;
    if (n == 0) {
        return 1;
    }

    long N = n;
    if (n < 0) {
        oneDivid = true;
        //负的Integer.MIN_VALUE依然为负,所以这里不可以用N = -n
        N = -N;
    }
    if (x < 0) {
        x = -x;
        if (N % 2 == 1) {
            isNegative = true;
        }
    }

    double curRet = x;
    double ret = 1;
    //拿N= 7 举例
    // ret 是 x的7次方
    //i=7时,进入循环 可以把 x的7次方拆分成  一个x(放入ret) 和 3个 x*x
    //i = 3. 此时可以把 3个x*x  拆分成  一个 x*x(放入ret)  和  1个 x*x*x*x
    //此时i= 1.此时 把 1个 x*x*x*x放入ret.
    //i=0跳出循环
    for (long i = N; i > 0; i /= 2) {
        if ((i % 2) == 1) {
            ret = ret * curRet;
        }
        curRet = curRet * curRet;
    }

    ret = isNegative ? -ret : ret;
    ret = oneDivid ? 1 / ret : ret;
    return ret;
}
```

#### [@@89. 格雷编码](https://leetcode-cn.com/problems/gray-code/)

镜像  01

00  01  |  在首位添1,同时镜像添加数据  11  10

00  01  11 10 |  110 111 101 100

```java
public List<Integer> grayCode(int n) {
    List<Integer> ret = new ArrayList<Integer>();
    if (n <= 0) {
        ret.add(0);
        return ret;
    }

    int headAdd = 1;
    ret.add(0);
    ret.add(1);

    for (int i = 2; i <= n; i++) {
        headAdd = headAdd * 2;
        for (int j = ret.size() - 1; j >= 0; j--) {
            ret.add(ret.get(j) + headAdd);
        }
    }
    return ret;
}
```

#### [@53. 最大子序和](https://leetcode-cn.com/problems/maximum-subarray/)

dp[i]表示以nums[i]结尾的和的最大值,而且前面丢不丢弃已经设置好了

```java
   public int maxSubArray(int[] nums) {
        if(nums==null||nums.length==0){
            return 0;
        }
        int len = nums.length;
        int [] dp = new int[len];
        dp[0] = nums[0];
        int max = nums[0];
        for(int i =1;i<len;i++){
            dp[i] =Math.max(dp[i-1]+nums[i],nums[i]);
            max = Math.max(dp[i],max);
        }
        return max;
    }
```

#### 单例模式

@构造器私有化

@双重校验+一个类锁

@内部遍历加volatile,static,private

volatile是为了防止指令重排,导致内存分配了,但是没有初始化,就把单例返回了回去

@获取单例的函数式static的

```java
private static volatile Singleton uniqueSingleton;

private Singleton() {
}

public static Singleton getUniqueSingleton() {
    if (uniqueSingleton == null) {
        synchronized (Singleton.class) {
            if (uniqueSingleton == null) {
                uniqueSingleton = new Singleton();
            }
        }
    }
    return uniqueSingleton;
}

public static void main(String[] args) {
    FileTypeMap defaultFileTypeMap = MimetypesFileTypeMap.getDefaultFileTypeMap();
    MimeType mimeType = new MimeType();
    mimeType.getBaseType();
}

```

#### [@@@@@752. 打开转盘锁](https://leetcode-cn.com/problems/open-the-lock/)

@@@@@@这种不求全部路径,而是求最短路径长度类的题目,一般是BFS.

因为找到最短路径的长度就可以退出了

带条件的BFS

用提前插入一个null来区分层次

```java
public int openLock(String[] deadends, String target) {

        HashSet<String> used = new HashSet<>();
        HashSet<String> dead = new HashSet<>();
        Queue<String> queue = new LinkedList<String>();
        for (int i = 0; i < deadends.length; i++) {
            dead.add(deadends[i]);
        }
        if (dead.contains("0000") || dead.contains(target)) {
            return -1;
        }
        int depth = 0;
        queue.add("0000");
        used.add("0000");
    //添加null,分开层数
        queue.add(null);
        while (!queue.isEmpty()) {
            String node = queue.poll();
            //如果node为空
            //加层数
            //如果queue里面还有node,加一个null来区分
            if (node == null) {
                depth++;
                if (!queue.isEmpty()) {
                    queue.add(null);
                } else {
                    break;
                }
            } else if (node.equals(target)) {
                return depth;
            } else {
                for (int i = 0; i < target.length(); i++) {
                    for (int d = -1; d <= 1; d = d + 2) {
                        int tempNum = node.charAt(i) - '0' + d;
                        //+10  再 %10
                        tempNum = (tempNum + 10) % 10;
                        //substring(0,0)也可以
                        String temp = node.substring(0, i) + tempNum + node.substring(i + 1);
                        //如果used  dead都符合条件
                        if (!used.contains(temp) && !dead.contains(temp)) {
                            used.add(temp);
                            queue.add(temp);
                        }
                    }
                }
            }
        }
        return -1;
    }
```

#### [掷骰子的数学期望](https://www.zhihu.com/question/30470526)

```
掷骰子，掷到几点就给几块钱，你觉得掷得小可以重来，但不能反悔，最多九次机会可以重来。掷骰子的人想获得最好的收益，什么样的策略最科学？
问题发散：骰子是1到6确定的数字，如果是一个不确定的随机范围，想获得最好的收益，什么样的策略比较科学？（人生中好多选择就类似这种模型，并不是同时提供所有的选择供你比较，而是当你拒绝一个选择，才会给你第二个选择，有的时候第二次随机的结果不如第一次，但是已经没有机会了）
```

```
从后往前想,
当我只剩最后一次掷骰子的时候,数学期望是3.5  ( 1+2+3+4+5+6)* 1/6
那我剩余两次的时候的数学期望是  (4+5+6) * 1/6  再加上  (3.5+3.5+3.5)*1/6  因为我如果掷出来1,2,3
由于下一次的数学期望高于1,2,3所以我遇到1,2,3会选择扔最后一次,最后一次的期望是3.5.
最后求出来,剩余两次的期望是4.25

```

公式为设剩余次数 n 时, 预期的点数期望为dp[n]

只要本次点数比剩余期望大就行.

<img src="pictures/LeetCode03/image-20200323141458248.png" alt="image-20200323141458248" style="zoom:55%;" />

<img src="pictures/LeetCode03/image-20200323141541535.png" alt="image-20200323141541535" style="zoom:50%;" />

#### [915. 分割数组](https://leetcode-cn.com/problems/partition-array-into-disjoint-intervals/)

```java
class Solution {
    public int partitionDisjoint(int[] A) {
        int N = A.length;
        int[] maxleft = new int[N];
        int[] minright = new int[N];

        int m = A[0];
        for (int i = 0; i < N; ++i) {
            m = Math.max(m, A[i]);
            maxleft[i] = m;
        }

        m = A[N-1];
        for (int i = N-1; i >= 0; --i) {
            m = Math.min(m, A[i]);
            minright[i] = m;
        }

        for (int i = 1; i < N; ++i)
            if (maxleft[i-1] <= minright[i])
                return i;

        throw null;
    }
}
```

#### [@@@@991. 坏了的计算器](https://leetcode-cn.com/problems/broken-calculator/)

这个题的贪心算法太取巧了,

@也可以用BFS来做

```java
public int brokenCalc(int X, int Y) {
    int ret = 0;
    while (Y > X) {
        ret++;
        if (Y % 2 == 1) {
            Y++;
        } else {
            Y /= 2;
        }
    }
    ret = ret + (X - Y);
    return ret;
}
```

#### [@@@@@227. 基本计算器 II](https://leetcode-cn.com/problems/basic-calculator-ii/)

```java
 public int calculate(String s) {
        int index = 0;
        int num = 0;
        char preCal = '+';
        int len = s.length();

        if (s.charAt(0) == '-') {
            index++;
        } else if (s.charAt(0) == '+') {
            index++;
        }
        Stack<Integer> stack = new Stack<Integer>();
        while (index <= len) {
            if (index < len && s.charAt(index) == ' ') {
                index++;
                continue;
            }
            if (index < len && s.charAt(index) >= '0' && s.charAt(index) <= '9') {
                num = num * 10 + s.charAt(index) - '0';
            } else {
                if (preCal == '-') {
                    stack.push(-num);
                } else if (preCal == '+') {
                    stack.push(num);
                } else if (preCal == '/') {
                    int temp = stack.pop();
                    stack.push(temp / num);
                } else if (preCal == '*') {
                    int temp = stack.pop();
                    stack.push(temp * num);
                }
                if (index < len) {
                    preCal = s.charAt(index);
                }
                num = 0;
            }
            index++;
        }
        int ret = 0;
        while (!stack.isEmpty()) {
            ret += stack.pop();
        }
        return ret;
    }
```

#### [@@删除回文子序列](https://leetcode-cn.com/problems/remove-palindromic-subsequences/)

```
回文子序列而非回文子串,先删除aaaa... 如果还有b就删除b
```

#### @@@@[Leetcode 1246：删除回文子数组](https://coordinate.wang/index.php/archives/2737/)

状态转移方程

```
 a  ****  a  cde

dp[start][end] = Math.min(dp[start][end],dp[start+1][k-1]+dp[k+1][end])

若start处的a等于k处的a.则删除k处的a和start处的a,可以被附加到子串start+1 ~k-1上,因为哪怕start+1~k-1
一个回文串也没有,比如只有cdf 此时dp[start+1][k-1] =3.  两个a依然可以被附加到 cdf任意一个回文串上来
删除.

```

<img src="pictures/LeetCode03/image-20200327193550768.png" alt="image-20200327193550768" style="zoom:50%;" />

```java
class Solution {
    private static int func(int[] nums) {
        int n = nums.length;
        int[][] dp = new int[n + 1][n + 1];
        for (int i = 0; i <= n; i++) {
            dp[i][i] = 1;
        }
        for (int l = 1; l <= n; l++) {
            for (int start = 0, end = l - 1; end < n; start++, end++) {
                for (int k = start; k <= end; k++) {
                    if (k == start) {
                        dp[start][end] = 1 + dp[start + 1][end];
                        continue;
                    } else if (k == start + 1 && nums[k] == nums[start]) {
                        dp[start][end] = Math.min(dp[start][end], 1 + dp[k + 1][end]);
                    } else if (k >= start + 2 && nums[k] == nums[start]) {
                        dp[start][end] = Math.min(dp[start][end], dp[start + 1][k - 1] + dp[k + 1][end]);
                    }
                }
            }

        }
        return dp[0][n - 1];
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int[] nums = new int[]{5, 9, 1, 3, 4, 1, 5};
        System.out.println(func(nums));
    }
}
```

#### [328. 奇偶链表](https://leetcode-cn.com/problems/odd-even-linked-list/)

<img src="pictures/LeetCode03/image-20200327193829378.png" alt="image-20200327193829378" style="zoom:50%;" />

#### [@@179. 最大数](https://leetcode-cn.com/problems/largest-number/)

```
错误!!!
public int compare(int n1,int n2){
	String num1 = ""+n1;
	String num2 = ""+n2;
	return num2.compareTo(num1);
}
```

**这样写是为了杜绝   50   和 5 的排序,**

**上面的写法,因为 5 小于50   所以排序为 505**

**下面的写法,因为505 小于 550  所以排序为 550**

```
public int compare(int n1,int n2){
	String order1 = ""+n1+n2;
	String order2 = ""+n2+n1;
	return order2.compareTo(order1);
}
```

#### [@@@69. x 的平方根](https://leetcode-cn.com/problems/sqrtx/)

@@ long square =  int * int.得到的依然是溢出的int

所以需要 long square = (long) mid * (long) mid;

 @@小于的时候,是mid本身
 left = mid;

@@二分法的思路

```java
public int mySqrt(int x) {
        if (x == 0) {
            return 0;
        }
        int left = 1;
        int right = x / 2;
        while (left < right) {
            int mid = left + (right - left) / 2 + 1;
            long square = (long) mid * (long) mid;
            if (square == x) {
                return mid;
            } else if (square > x) {
                right = mid - 1;
            } else {
                //小于的时候,是mid本身
                left = mid;
            }
        }
        return left;
    }
```

#### @@@@@@从十亿数字中,求第k位数字,用快排.

从十亿数字中,求第k位数字,用**快排**

求前k位大的数字,用一个大小为k的**小顶堆**  或者 **快排**

#### [@@973. 最接近原点的 K 个点](https://leetcode-cn.com/problems/k-closest-points-to-origin/)

1.求平方和用long来存储.

2.只求平方和就行,不需要sqrt

3.总体思路: 求平方和,排序,求第k大的平方和,再扫一遍求出前k个.

#### [1375. 灯泡开关 III](https://leetcode-cn.com/problems/bulb-switcher-iii/)

```java
思路1:模拟
public int numTimesAllBlue(int[] light) {
    int onCutIndex = 0;
    int blueMomentCount = 0;
    PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10, (o1, o2) -> (o1 - o2));
    for (int i = 0; i < light.length; i++) {
        int curLight = light[i];
        if (curLight == onCutIndex + 1) {
            onCutIndex++;
            while (!queue.isEmpty() && onCutIndex + 1 == queue.peek()) {
                queue.poll();
                onCutIndex++;
            }
            if (queue.isEmpty()) {
                blueMomentCount++;
            }
        } else {
            queue.add(curLight);
        }
    }
    return blueMomentCount;
}

思路2: // 若点亮最远的灯的位置大于此时点亮灯的个数，则意味着在点亮最远的灯的位置之前存在着未点亮的灯。

class Solution {
    //这题关键是读懂题目含义，
    // 让所有开着的灯都变成蓝色的时刻的性质是：此时点亮最远的灯的位置恰巧等于点亮灯的个数。
    // 若点亮最远的灯的位置大于此时点亮灯的个数，则意味着在点亮最远的灯的位置之前存在着未点亮的灯。
    public int numTimesAllBlue(int[] light) {
        int size = light.length;
        int count = 0, maxReachingPoint = 0;
        for (int i = 0 ; i < size; i++){
            maxReachingPoint = Math.max(maxReachingPoint, light[i]);
            if ( i + 1 == maxReachingPoint){
                count += 1;
            }
        }
        return count;
    }
```

#### [@@581. 最短无序连续子数组](https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/)

```java
public int findUnsortedSubarray(int[] nums) {
    int len = nums.length;
    int[] leftMax = new int[len];
    int[] rightMin = new int[len];
    int max = nums[0];
    int min = nums[len - 1];
    leftMax[0] = max;
    rightMin[len - 1] = min;
    for (int i = 0; i < len; i++) {
        if (nums[i] > max) {
            max = nums[i];

        }
        leftMax[i] = max;
    }
    for (int i = len - 1; i >= 0; i--) {
        if (nums[i] < min) {
            min = nums[i];
        }
        rightMin[i] = min;
    }
    int leftCut = 0;
    int rightCut = len - 1;
    for (int i = 0; i < len; i++) {
        if (leftMax[i] > rightMin[i]) {
            leftCut = i;
            break;
        }
    }
    for (int i = len - 1; i >= 0; i--) {
        if (rightMin[i] < leftMax[i]) {
            rightCut = i;
            //应对 3  2  1
            return rightCut - leftCut + 1;
        }
    }
    //应对  1 2  3
    return 0;
}
```

#### [@@@@@@769. 最多能完成排序的块](https://leetcode-cn.com/problems/max-chunks-to-make-sorted/)

一个区间内最大的数字，不应该大于这个区间最右边的index。因此我们从左向右遍历，如果已经观测到的最大值小于等于这个区间的index，则可以划分区间。

```java
class Solution {
    public int maxChunksToSorted(int[] arr) {
        int ans = 0, max = 0;
        for (int i = 0; i < arr.length; ++i) {
            max = Math.max(max, arr[i]);
            if (max == i) ans++;
        }
        return ans;
    }
}

```

#### @@元素的技巧(如果大于左边最大值,小于右边最小值,该元素排序后依然在该位置上)

#### @@缝隙的技巧(如果大于前缀最大值,小于后缀最小值,则是分界元素)

#### @@@@划分最多的最短无序连续子数组

作者：Ire1ia
链接：https://www.nowcoder.com/discuss/373413?type=post&order=create&pos=&page=2
来源：牛客网

算法题: 一个整数数组, 将这个数组划分为一些连续的子数组, 将各个子数组内部元素进行排序后，整个数组是有序的，问最多能切成多少个子数组. 这题确实没见过, 不知道是不是leetcode上的, 我先是用dp写了个O(n^2)的, 面试官在看我的代码, 我趁这个时间又思考了几分钟, 发现这题可以用O(n)的贪心解决(对于每个缝隙, 如果前缀的最大值小于等于后缀的最小值, 那么就可以在这里切),

```java
 public int findUnsortedSubarray(int[] nums) {
        int len = nums.length;
        //len个数字,有len-1个缝隙,在每个缝隙处,如果左边的最大值小于右边的最小值,就可以从这里切分
        //原因:缝隙两边的数字不会越过缝隙,
        int[] leftMax = new int[len - 1];
        int[] rightMin = new int[len - 1];
        int max = nums[0];
        int min = nums[len - 1];

        leftMax[0] = max;
        for (int i = 1; i <= len - 2; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
            leftMax[i] = max;
        }

        //缝隙是0~len-2
        // 缝隙为i的时候,缝隙右边的数字为i+1
        rightMin[len - 2] = min;
        for (int i = len - 3; i >= 0; i--) {
            if (nums[i + 1] < min) {
                min = nums[i + 1];
            }
            rightMin[i] = min;
        }

        List<Integer> list = new ArrayList<>();
        int cnt = 0;
        //如果缝隙处,如果左边的最大值 小于等于  右边的最小值,就可以从这里切分
        //原因:缝隙两边的数字不会越过缝隙,
        for (int i = 0; i <= len - 2; i++) {
            if (leftMax[i] <= rightMin[i]) {
                list.add(i);
                cnt++;
            }
        }
        //输出切分的缝隙
        for (int temp : list) {
            System.out.println(temp);
        }
        return cnt + 1;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        Solution solution = new Solution();
        int i = solution.findUnsortedSubarray(new int[]{3, 9, 9});
        System.out.println(i);
    }
```

#### [@@292. Nim 游戏](https://leetcode-cn.com/problems/nim-game/)

```
推理

让我们考虑一些小例子。显而易见的是，如果石头堆中只有一块、两块、或是三块石头，那么在你的回合，你就可以把全部石子拿走，从而在游戏中取胜。而如果就像题目描述那样，堆中恰好有四块石头，你就会失败。因为在这种情况下不管你取走多少石头，总会为你的对手留下几块，使得他可以在游戏中打败你。因此，要想获胜，在你的回合中，必须避免石头堆中的石子数为 4 的情况。

同样地，如果有五块、六块、或是七块石头，你可以控制自己拿取的石头数，总是恰好给你的对手留下四块石头，使他输掉这场比赛。但是如果石头堆里有八块石头，你就不可避免地会输掉，因为不管你从一堆石头中挑出一块、两块还是三块，你的对手都可以选择三块、两块或一块，以确保在再一次轮到你的时候，你会面对四块石头。

显然，它以相同的模式不断重复 n=4,8,12,16,\dotsn=4,8,12,16,…，基本可以看出是 44 的倍数。

作者：LeetCode
链接：https://leetcode-cn.com/problems/nim-game/solution/nimyou-xi-by-leetcode/
来源：力扣（LeetCode）
著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。
```

#### [36. 有效的数独](https://leetcode-cn.com/problems/valid-sudoku/)

9个行map,9个列map,9个boxmap,

```java
HashMap<Integer, Integer> [] rows = new HashMap[9];
    HashMap<Integer, Integer> [] columns = new HashMap[9];
    HashMap<Integer, Integer> [] boxes = new HashMap[9];
    for (int i = 0; i < 9; i++) {
      rows[i] = new HashMap<Integer, Integer>();
      columns[i] = new HashMap<Integer, Integer>();
      boxes[i] = new HashMap<Integer, Integer>();
    }

```

#### [150. 逆波兰表达式求值](https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/)

```java
挺简单的,主要注意
    乘法要考虑溢出问题

public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();
        Integer op1, op2;
        for (String s : tokens) {
            if (s.equals("+")) {
                op2 = stack.pop();
                op1 = stack.pop();
                stack.push(op1 + op2);

            } else if (s.equals("-")) {
                op2 = stack.pop();
                op1 = stack.pop();
                stack.push(op1 - op2);
            } else if (s.equals("*")) {
                op2 = stack.pop();
                op1 = stack.pop();
                stack.push(op1 * op2);
            } else if (s.equals("/")) {
                op2 = stack.pop();
                op1 = stack.pop();
                stack.push(op1 / op2);
            } else {
                stack.push(Integer.valueOf(s));
            }
        }
        return stack.pop();
    }
```

#### [@@@@659. 分割数组为连续子序列](https://leetcode-cn.com/problems/split-array-into-consecutive-subsequences/)

```java
@@@@@注意要先从小数字开始!!!!!!!!!!
思路:如果tail[cur-1]存在,则把cur接到后面,如果不存在,则观察 cur+1  cur+2 存在则 tain[cur+2]+1

public boolean isPossible(int[] nums) {
    HashMap<Integer, Integer> numCount = new HashMap<>();
    HashMap<Integer, Integer> tailMemo = new HashMap<>();
    for (int cur : nums) {
        numCount.put(cur, numCount.getOrDefault(cur, 0) + 1);
    }
    for (int cur : nums) {
        if (numCount.getOrDefault(cur, 0) == 0) {
            continue;
        } else if (tailMemo.getOrDefault(cur - 1, 0) > 0) {
            numCount.put(cur, numCount.get(cur) - 1);

            tailMemo.put(cur - 1, tailMemo.get(cur - 1) - 1);
            tailMemo.put(cur, tailMemo.getOrDefault(cur, 0) + 1);
        } else if (numCount.getOrDefault(cur + 1, 0) > 0 && numCount.getOrDefault(cur + 2, 0) > 0) {
            numCount.put(cur, numCount.get(cur) - 1);
            numCount.put(cur + 1, numCount.get(cur + 1) - 1);
            numCount.put(cur + 2, numCount.get(cur + 2) - 1);

            tailMemo.put(cur + 2, tailMemo.getOrDefault(cur + 2, 0) + 1);
        } else {
            return false;
        }
    }
    return true;
}

比较清晰的写法

    class Solution {
    public boolean isPossible(int[] nums) {
        Counter count = new Counter();
        Counter tails = new Counter();
        for (int x: nums) count.add(x, 1);

        for (int x: nums) {
            if (count.get(x) == 0) {
                continue;
            } else if (tails.get(x) > 0) {
                tails.add(x, -1);
                tails.add(x+1, 1);
            } else if (count.get(x+1) > 0 && count.get(x+2) > 0) {
                count.add(x+1, -1);
                count.add(x+2, -1);
                tails.add(x+3, 1);
            } else {
                return false;
            }
            count.add(x, -1);
        }
        return true;
    }
}

class Counter extends HashMap<Integer, Integer> {
    public int get(int k) {
        return containsKey(k) ? super.get(k) : 0;
    }

    public void add(int k, int v) {
        put(k, get(k) + v);
    }
}
```

#### [@@@@@@877. 石子游戏](https://leetcode-cn.com/problems/stone-game/)

```java
博弈题目

  //  状态转移:先手:
dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)
dp[i][j].fir = max(    选择最左边的石头堆     ,     选择最右边的石头堆     )
# 解释：我作为先手，面对 piles[i...j] 时，有两种选择：
# 要么我选择最左边的那一堆石头，然后面对 piles[i+1...j]
# 但是此时轮到对方，相当于我变成了后手；
# 要么我选择最右边的那一堆石头，然后面对 piles[i...j-1]
# 但是此时轮到对方，相当于我变成了后手。

  //  状态转义:后手:
if 先手选择左边:
    dp[i][j].sec = dp[i+1][j].fir
if 先手选择右边:
    dp[i][j].sec = dp[i][j-1].fir
# 解释：我作为后手，要等先手先选择，有两种情况：
# 如果先手选择了最左边那堆，给我剩下了 piles[i+1...j]
# 此时轮到我，我变成了先手；
# 如果先手选择了最右边那堆，给我剩下了 piles[i...j-1]
# 此时轮到我，我变成了先手。

 //base  case

dp[i][j].fir = piles[i]
dp[i][j].sec = 0
其中 0 <= i == j < n
# 解释：i 和 j 相等就是说面前只有一堆石头 piles[i]
# 那么显然先手的得分为 piles[i]
# 后手没有石头拿了，得分为 0

public boolean stoneGame(int[] piles) {
        int n = piles.length;
        int[][][] dp = new int[n][n][2];
        //边界
        for (int i = 0; i < n; i++) {
            dp[i][i][0] = piles[i];
            dp[i][i][1] = 0;
        }
        for (int l = 2; l <= n; l++) {
            //n - l
            for (int i = 0; i <= n - l; i++) {
                int j = l + i - 1;
                int left = piles[i] + dp[i + 1][j][1];
                int right = piles[j] + dp[i][j - 1][1];
                if (left > right) {
                    dp[i][j][0] = left;
                    dp[i][j][1] = dp[i + 1][j][0];
                } else {
                    dp[i][j][0] = right;
                    dp[i][j][1] = dp[i][j - 1][0];
                }
            }
        }
        return (dp[0][n - 1][0] - dp[0][n - 1][1]) > 0 ? true : false;
    }
```

#### [@@编程算法题—硬币游戏](https://blog.csdn.net/guangyacyb/article/details/80173509)

```java
题目：两个玩家（甲，乙）玩游戏，有一个长度为n的乱序数组（数组每个元素代表一个硬币，数值代表硬币面值），甲乙轮流拿硬币（可以拿走最左边的一个或者两个硬币，拿走不放回），问先玩的玩家是否一定能赢

这种题目的思路一般是动态规划：

假设存有硬币的数组为a，并且双方都会取当前情况下能取到的最多数值！！！

为了方便，a[i]表示从末尾开始的第i枚硬币的数值

用sum[i]表示从0开始到下标i的硬币数字和

best[i]表示当取到剩下i枚硬币时最优的取法能取到的数值

所以有：

best[1]是第1枚硬币的数值

best[2]是第1+2枚硬币的数值之和

best[3]是第2+3枚硬币的数值之和

best[4]有两种取法，一种是取a[4]，然后因为对方也不是吃素的，所以他会取best[3]，所以我能取到的只能是sum[3]-best[3]；另一种取法是取a[4]+a[3]，然后对方取best[2]，我取sum[2]-best[2]，这就基本是动态规划的情况了

以上归纳为：

best[k]（k>4）有两种取法:

1：取a[k] +sum[k-1]-best[k-1]

2： 取a[k] + a[k-1] + sum[k-2] + best[k-2]

则best[k]等于这两个取法的较大值，最后求出best[n]，如果best[n] > sum[n] / 2的话，则先取的人赢了，否则会输。至于是否先取就能赢，还没想到，烦请路过的网友给点提示~\
```

#### [5369. 统计作战单位数](https://leetcode-cn.com/problems/count-number-of-teams/)

```JAVA
public int numTeams(int[] rating) {
    int ret = 0;
    for (int i = 1; i <= rating.length - 2; i++) {
        int biggerLeft = 0;
        int smallerLeft = 0;
        int biggerRight = 0;
        int smallerRight = 0;
        int curRate = rating[i];
        for (int j = 0; j  curRate) {
                biggerLeft++;
            } else {
                smallerLeft++;
            }
        }
        for (int j = rating.length - 1; j > i; j--) {
            if (rating[j] > curRate) {
                biggerRight++;
            } else {
                smallerRight++;
            }
        }
        ret += biggerLeft * smallerRight + smallerLeft * biggerRight;
    }
    return ret;
}
```

#### [5370. 设计地铁系统](https://leetcode-cn.com/problems/design-underground-system/)

```java
class UndergroundSystem {
    private HashMap<Integer, String> id_stationName = new HashMap<>();
    private HashMap<Integer, Integer> id_t = new HashMap<>();
    private HashMap<String, Double> trip_TimeSum = new HashMap<>();
    private HashMap<String, Integer> trip_CountSum = new HashMap<>();

    public UndergroundSystem() {

    }

    public void checkIn(int id, String stationName, int t) {
        id_t.put(id, t);
        id_stationName.put(id, stationName);
    }

    public void checkOut(int id, String stationName, int t) {
        String startStation = id_stationName.get(id);
        Integer startTime = id_t.get(id);

        String tripName = startStation + "#" + stationName;
        Integer tripTime = t - startTime;

        if (trip_TimeSum.containsKey(tripName)) {
            trip_TimeSum.put(tripName, trip_TimeSum.get(tripName) + (double) tripTime);
            trip_CountSum.put(tripName, trip_CountSum.get(tripName) + 1);
        } else {
            trip_TimeSum.put(tripName, (double) tripTime);
            trip_CountSum.put(tripName, 1);
        }
    }

    public double getAverageTime(String startStation, String endStation) {
        String tripName = startStation + "#" + endStation;
        if (trip_TimeSum.containsKey(tripName)) {
            Double tripTime = trip_TimeSum.get(tripName);
            Integer count = trip_CountSum.get(tripName);
            return tripTime / count;
        }
        return 0;
    }
}
```

#### 海量数据的排序

分成几份,每份快排

8路合并  合成 5 路

5路再做合并合成一路

```
1、外排序
　　传统的排序算法一般指内排序算法，针对的是数据可以一次全部载入内存中的情况。但是面对海量数据，即数据不可能一次全部载入内存，需要用到外排序的方法。外排序采用分块的方法（分而治之），首先将数据分块，对块内数据按选择一种高效的内排序策略进行排序。然后采用归并排序的思想对于所有的块进行排序，得到所有数据的一个有序序列。

　　例如，考虑一个1G文件，可用内存100M的排序方法。首先将文件分成10个100M，并依次载入内存中进行排序，最后结果存入硬盘。得到的是10个分别排序的文件。接着从每个文件载入9M的数据到输入缓存区，输出缓存区大小为10M。对输入缓存区的数据进行归并排序，输出缓存区写满之后写在硬盘上，缓存区清空继续写接下来的数据。对于输入缓存区，当一个块的9M数据全部使用完，载入该块接下来的9M数据，一直到所有的9个块的所有数据都已经被载入到内存中被处理过。最后我们得到的是一个1G的排序好的存在硬盘上的文件。

2、1TB数据使用32GB内存如何排序
　　①、把磁盘上的1TB数据分割为40块（chunks），每份25GB。（注意，要留一些系统空间！）
　　②、顺序将每份25GB数据读入内存，使用quick sort算法排序。
　　③、把排序好的数据（也是25GB）存放回磁盘。
　　④、循环40次，现在，所有的40个块都已经各自排序了。（剩下的工作就是如何把它们合并排序！）
　　⑤、从40个块中分别读取25G/40=0.625G入内存（40 input buffers）。
　　⑥、执行40路合并，并将合并结果临时存储于2GB 基于内存的输出缓冲区中。当缓冲区写满2GB时，写入硬盘上最终文件，并清空输出缓冲区；当40个输入缓冲区中任何一个处理完毕时，写入该缓冲区所对应的块中的下一个0.625GB，直到全部处理完成。

3、继续优化
　　磁盘I/O通常是越少越好（最好完全没有），那么如何降低磁盘I/O操作呢？关键就在第5和第6步中的40路输入缓冲区，我们可以先做8路merge sort，把每8个块合并为1路，然后再做5-to-1的合并操作。
　　再深入思考一下，如果有多余的硬件，如何继续优化呢？有三个方向可以考虑：
　　使用并发：如多磁盘（并发I/O提高）、多线程、使用异步I/O、使用多台主机集群计算。
　　提升硬件性能：如更大内存、更高RPM的磁盘、升级为SSD、Flash、使用更多核的CPU。
　　提高软件性能：比如采用radix sort、压缩文件（提高I/O效率）等。
————————————————
版权声明：本文为CSDN博主「无鞋童鞋」的原创文章，遵循 CC 4.0 BY-SA 版权协议，转载请附上原文出处链接及本声明。
原文链接：https://blog.csdn.net/FX677588/article/details/72471357
```

#### [@@@146. LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/)

@@注意head和tail在初始化的时候,要连接起来

```java
class LRUCache {
    int capacity = 0;
    DoubleList list = null;
    HashMap<Integer, Node> map = null;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        list = new DoubleList();
        map = new HashMap<>();
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        Node node = map.get(key);
        list.remove(node);
        list.addFirst(node);
        return node.val;
    }

    public void put(int key, int val) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.key = key;
            node.val = val;
            list.remove(node);
            list.addFirst(node);
            return;
        }
        Node node = new Node(key, val);
        list.addFirst(node);
        map.put(key, node);
        if (list.size > capacity) {
            Node toDel = list.removeLast();
            map.remove(toDel.key);
        }
    }
}

class DoubleList {
    int size;
    Node head, tail;

    public DoubleList() {
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.pre = head;
        size = 0;

    }

    public void addFirst(Node node) {
        Node nextNode = head.next;
        node.pre = head;
        node.next = nextNode;
        head.next = node;
        nextNode.pre = node;
        size++;
    }

    public void remove(Node node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
        size--;
    }

    public Node removeLast() {
        if (tail.pre == head) {
            return null;
        }
        Node toDel = tail.pre;
        remove(toDel);
        return toDel;
    }
}

class Node {
    Node pre, next;
    int key, val;
    public Node(int key, int val) {
        this.key = key;
        this.val = val;
    }
}
```

#### [@@@@140. 单词拆分 II](https://leetcode-cn.com/problems/word-break-ii/)

```java
public class Solution {
   public List<String> wordBreak(String s, Set<String> wordDict) {
       LinkedList<String>[] dp = new LinkedList[s.length() + 1];
       LinkedList<String> initial = new LinkedList<>();
       initial.add("");
       dp[0] = initial;
       for (int i = 1; i <= s.length(); i++) {
           LinkedList<String> list = new LinkedList<>();
           for (int j = 0; j  0 && wordDict.contains(s.substring(j, i))) {
                   for (String l : dp[j]) {
                       list.add(l + (l.equals("") ? "" : " ") + s.substring(j, i));
                   }
               }
           }
           dp[i] = list;
       }
       return dp[s.length()];
   }
}

```

#### 把字符串中重复的子串删除

```java
//思路

//ssabxx

left  pre
left left+1

//case
//12313
//11123
//23111
//ssxx
//空
//xxxx
//

//优化
stringbuilder ret

//left pre = left+1
//sx11121
//
public String deleteDuplicateChar(String s){
   if(s==null||s.length()==0){
       return "";
   }
   if(s.length()==1){
      return s;
   }
   StringBuilder ret = new StringBuilder();
   char[] chs  = s.toCharArray();

   int left = 0;
   int len = chs.length();
    //ab
   while(left<len){
       if(left+1<len&&chs[left]==chs[left+1]){
           char deadCh = chs[left];
           while(chs[left]==deadCh&&left<len){
               left++;
           }
       }else{
           ret.append(chs[left]);
           left++;
       }
   }
   return ret.toString();
}
```

#### 时光倒流

给出周时分,和要倒流的分钟数k,

求倒流之后的时间是多少

举例:

输入

2 09:50  60

输出

2 08:50

解释

周二九点五十,倒流60分钟,是周二八点五十

```java
public static void main(String[] args) {
    int[] pre = new int[3];
    int[] now = new int[3];
    int[] deta = new int[3];
    Scanner scanner = new Scanner(System.in);
    int week = scanner.nextInt();
    String str = scanner.next();
    int minDeta = scanner.nextInt();
    int hour = Integer.parseInt(str.substring(0, 2));
    int min = Integer.parseInt(str.substring(3, 5));
    now[0] = week;
    now[1] = hour;
    now[2] = min;
    deta[2] = minDeta % 60;
    deta[1] = minDeta / 60 % 24;
    deta[0] = minDeta / (60 * 24) % 7;

    if (now[2] >= deta[2]) {
        pre[2] = now[2] - deta[2];
    } else {
        pre[2] = (now[2] + 60 - deta[2]);
        now[1]--;
    }
    if (now[1] >= deta[1]) {
        pre[1] = now[1] - deta[1];
    } else {
        pre[1] = (now[1] + 24 - deta[1]);
        now[0]--;
        if (now[0] == 0) {
            now[0] = 7;
        }
    }

    if (now[0] >= deta[0]) {
        pre[0] = now[0] - deta[0];
    } else {
        pre[0] = (now[0] + 7 - deta[0]);
    }
    System.out.println(pre[0]);
    StringBuilder time = new StringBuilder();
    if (pre[1] <= 9) {
        time.append("0" + pre[1] + ":");
    } else {
        time.append(pre[1] + ":");
    }
    if (pre[2] <= 9) {
        time.append("0" + pre[2]);
    } else {
        time.append(pre[2]);
    }
    System.out.println(time);

}
```

#### 正四面体游走

（四个顶点六条边）四个点为 ABCS，每条边可以走无数次,从顶点S出发K次能到达S点的可能性（K=1e6，对p=1e9+7取模）

比如 k =3,路径可以有

SBAS

SABS

SCBS

SBCS

SCAS

SACS

```java

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int k = scanner.nextInt();
        if (k == 0) {
            System.out.println(1);
            return;
        }
        int mod = 1000000000 + 7;
        //012 abc 3 s
        int[][] dp = new int[k + 1][4];
        dp[1][0] = 1;
        dp[1][1] = 1;
        dp[1][2] = 1;
        dp[1][3] = 0;
        for (int i = 2; i <= k; i++) {
            for (int j = 0; j < 4; j++) {
                for (int m = 0; m < 4; m++) {
                    if (m != j) {
                        dp[i][j] = (dp[i][j] % mod + dp[i - 1][m] % mod) % mod;
                    }
                }
            }
        }
        System.out.println(dp[k][3]);

    }
```

<file_sep>/notes/Netty.md

第一章：高并发编程Netty实战课程介绍

1、异步事件驱动NIO框架Netty介绍

    简介：介绍Netty来源，版本，目前在哪些主流公司和产品框架使用

    1、Netty是由JBOSS提供的一个java开源框架, 是业界最流行的NIO框架，整合了多种协议（

    包括FTP、SMTP、HTTP等各种二进制文本协议）的实现经验，精心设计的框架，在多个大型商业项目中得到充分验证。

        1）API使用简单

        2）成熟、稳定

        3）社区活跃  有很多种NIO框架 如mina

        4）经过大规模的验证（互联网、大数据、网络游戏、电信通信行业）

    2、那些主流框架产品在用？

        1）搜索引擎框架 ElasticSerach

        1) Hadopp子项目Avro项目，使用Netty作为底层通信框架

        3）阿里巴巴开源的RPC框架 Dubbo

            地址：http://dubbo.apache.org/zh-cn/

            Netty在Dubbo里面使用的地址

                https://github.com/apache/incubator-dubbo/tree/master/dubbo-remoting/dubbo-remoting-netty4/src/main/java/org/apache/dubbo/remoting/transport/netty4

        补充：netty4是dubbo2.5.6后引入的，2.5.6之前的netty用的是netty3

3、高并发编程Netty实战课程开发环境准备

    简介：讲解Netty实战开发环境

    1、IDEA旗舰版/Eclipse  + JDK8 + Maven3.5以上版本 + Netty4.x

    Netty版本说明

        采用最新的4.x版本，只要大版本一致就可以

        官方文档: https://netty.io/wiki/user-guide-for-4.x.html

        Github地址：https://github.com/netty/netty

第二章：使用JDK自带BIO编写一个Client-Server通信

1、BIO网络编程实战之编写BioServer服务端

    简介: 使用jdk自带的Bio编写一个统一时间服务

2、BIO网络编程实战之编写BioClient客服端

    简介：使用BIO网络编程编写BioClient客户端

3、BIO编写Client/Server通信优缺点分析

    简介：讲解BIO的优缺点，为啥不能高并发情况下性能弱

    优点：

        模型简单

        编码简单

    缺点:

        性能瓶颈，请求数和线程数 N：N关系

        高并发情况下，CPU切换线程上下文损耗大

    案例：web服务器Tomcat7之前，都是使用BIO，7之后就使用NIO

    改进：伪NIO,使用线程池去处理业务逻辑

第三章：(面试核心)服务端网络编程常见网络IO模型讲解

    1、最通俗的方式讲解 什么是阻塞/非阻塞，什么是同/异步

        简介：使用最通俗概念讲解 同步异步、堵塞和非堵塞

        阻塞与非阻塞 是指可不可以自己分配任务后干自己的事
        同步与异步  同步需要主动读写数据,异步仅需要i/o操作完成的通知

        洗衣机洗衣服

        洗衣机洗衣服（无论阻塞式IO还是非阻塞式IO，都是同步IO模型）

        同步阻塞：你把衣服丢到洗衣机洗，然后看着洗衣机洗完，洗好后再去晾衣服（你就干等，啥都不做，阻塞在那边）(主管盯着你干活,等你干完了再去写自己的代码)

        同步非阻塞：你把衣服丢到洗衣机洗，然后会客厅做其他事情，定时去阳台看洗衣机是不是洗完了，洗好后再去晾衣服(主管分给你任务就去写自己的代码了,但你的积极性不高,写完代码了也不汇报,需要主管经常来问你小孙代码写完了没)

        （等待期间你可以做其他事情，比如用电脑打开小D课堂看视频学习）

        异步阻塞: 你把衣服丢到洗衣机洗，然后看着洗衣机洗完，洗好后再去晾衣服（既然异步做自己的事情去了,就不会阻塞着啥事都做不了;几乎没这个说法，可以忽略）

        异步非阻塞：你把衣服丢到洗衣机洗，然后会客厅做其他事情，洗衣机洗好后会自动去晾衣服，晾完成后放个音乐告诉你洗好衣服并晾好了(主管分给小孙任务就去写自己的代码,小孙写完代码之后就告诉主管,我把代码写完了,也持续集成发布完成了,主管请验收)

异步非阻塞中, 非阻塞相对于阻塞的改进点是:不需要等待任务运行完,线程可以在任务运行时候,去做其他的事情;

异步相对与同步的改进点是: 异步的任务可以把所有东西都做好之后,通知父进程自己把任务做好了,验收就行了;而同步的任务完成的过程中需要父进程监督,完成了之后需要父进程来发现,并处理.

    2、(BAT面试核心知识)Linux网络编程中的五种I/O模型讲解上集

        简介：linux网络编程中的IO模型讲解上集

        网络IO,用户程序和内核的交互为基础进行讲解

        IO操作分两步：发起IO请求等待数据准备，实际IO操作（洗衣服，晾衣服）

        同步须要主动读写数据，在读写数据的过程中还是会阻塞（好比晾衣服阻塞了你）

        异步仅仅须要I/O操作完毕的通知。并不主动读写数据，由操作系统内核完毕数据的读写（机器人帮你自动晾衣服）

        五种IO的模型：阻塞IO、非阻塞IO、多路复用IO、信号驱动IO和异步IO

        前四种都是同步IO，在内核数据copy到用户空间时都是阻塞的

        权威：RFC标准，或者书籍 《UNIX Network Programming》中文名《UNIX网络编程-卷一》第六章

            1）阻塞式I/O；

            2）非阻塞式I/O；

            3）I/O复用（select，poll，epoll...）；

                I/O多路复用是阻塞在select，epoll这样的系统调用，没有阻塞在真正的I/O系统调用如recvfrom

                进程受阻于select,等待可能多个套接口中的任一个变为可读

                IO多路复用使用两个系统调用(select和recvfrom)

                blocking IO只调用了一个系统调用(recvfrom)

                select/epoll 核心是可以同时处理多个connection，而不是更快，所以连接数不高的话，性能不一定比多线程+阻塞IO好

                多路复用模型中，每一个socket，设置为non-blocking,

                阻塞是被select这个函数block，而不是被socket阻塞的

阻塞式I/O
![阻塞I/O](/Users/sunxu/Downloads/netty-projects-master/pic/Snipaste_2019-07-21_13-47-43.jpg)

非阻塞式I/O
![](/Users/sunxu/Downloads/netty-projects-master/pic/Snipaste_2019-07-21_13-51-33.jpg)

多路复用I/O(一个线程轮询多个请求)
![](/Users/sunxu/Downloads/netty-projects-master/pic/Snipaste_2019-07-21_13-53-47.jpg)

异步非阻塞
![](/Users/sunxu/Downloads/netty-projects-master/pic/Snipaste_2019-07-21_14-43-18.jpg)

    3、(BAT面试核心知识)Linux网络编程中的五种I/O模型讲解下集

        简介：linux网络编程中的IO模型讲解下集

            4）信号驱动式I/O（SIGIO）；

            5）异步I/O（POSIX的aio\_系列函数） Future-Listener机制；

        IO操作分为两步

            1）发起IO请求，等待数据准备(Waiting for the data to be ready)

            2）实际的IO操作，将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

            前四种IO模型都是同步IO操作，区别在于第一阶段，而他们的第二阶段是一样的：在数据从内核复制到应用缓冲区期间（用户空间），进程阻塞于recvfrom调用或者select()函数。 相反，异步I/O模型在这两个阶段都要处理。

            阻塞IO和非阻塞IO的区别在于第一步，发起IO请求是否会被阻塞，如果阻塞直到完成那么就是传统的阻塞IO，如果不阻塞，那么就是非阻塞IO。

            同步IO和异步IO的区别就在于第二个步骤是否阻塞，如果实际的IO读写阻塞请求进程，那么就是同步IO，因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO，如果不阻塞，而是操作系统帮你做完IO操作再将结果返回给你，那么就是异步IO。

            解读上面两段话:
            阻塞IO与异步IO都是针对IO而言的,阻塞的意思是用户进程发送请求后,是否阻塞不能从事其他工作;异步IO的意思是数据是否被其他进程异步拷贝到了用户进程

        几个核心点：

            阻塞非阻塞说的是线程的状态（重要）

            同步和异步说的是消息的通知机制（重要）

            同步需要主动读写数据,异步是不需要主动读写数据

            同步IO和异步IO是针对用户应用程序和内核的交互

    (重点,再深挖源码)
    4、 高并发编程必备知识IO多路复用技术select、poll讲解

        简介：高并发编程必备知识IO多路复用技术select、poll讲解

        什么是IO多路复用：

            I/O多路复用，I/O是指网络I/O, 多路指多个TCP连接(即socket或者channel），复用指复用一个或几个线程。

            简单来说：就是使用一个或者几个线程处理多个TCP连接

            最大优势是减少系统开销小，不必创建过多的进程/线程，也不必维护这些进程/线程

![](/Users/sunxu/Downloads/netty-projects-master/pic/Snipaste_2019-07-21_15-00-28.jpg)

        select：

            基本原理：

                监视文件3类描述符： writefds、readfds、和exceptfds

                调用后select函数会阻塞住，等有数据 可读、可写、出异常 或者 超时 就会返回

                select函数正常返回后，通过遍历fdset整个数组才能发现哪些句柄发生了事件，来找到就绪的描述符fd，然后进行对应的IO操作

                几乎在所有的平台上支持，跨平台支持性好

            缺点：

                1）select采用轮询的方式扫描文件描述符，全部扫描，随着文件描述符FD数量增多而性能下降

                2）每次调用 select()，需要把 fd 集合从用户态拷贝到内核态，并进行遍历(消息传递都是从内核到用户空间)

                2）最大的缺陷就是单个进程打开的FD有限制，默认是1024   （可修改宏定义，但是效率仍然慢）

                    static final  int MAX\_FD = 1024

        poll:

            基本流程：

                select() 和 poll() 系统调用的大体一样，处理多个描述符也是使用轮询的方式，根据描述符的状态进行处理

                一样需要把 fd 集合从用户态拷贝到内核态，并进行遍历。

                最大区别是: poll没有最大文件描述符限制（使用链表的方式存储fd）

5、 高并发编程必备知识IO多路复用技术Epoll讲解和总结

        简介：高并发编程必备知识IO多路复用技术epoll讲解和总结

        epoll：

            基本原理：

                在2.6内核中提出的，对比select和poll，epoll更加灵活，没有描述符限制，用户态拷贝到内核态只需要一次

                使用事件通知，通过epoll\_ctl注册fd，一旦该fd就绪，内核就会采用callback的回调机制来激活对应的fd

            优点：

                1)没fd这个限制，所支持的FD上限是操作系统的最大文件句柄数，1G内存大概支持10万个句柄

                2)效率提高，使用回调通知而不是轮询的方式，不会随着FD数目的增加效率下降

                2)通过callback机制通知，内核和用户空间mmap同一块内存实现

            Linux内核核心函数

                1)epoll\_create()  在Linux内核里面申请一个文件系统 B+树，返回epoll对象，也是一个fd

                2)epoll\_ctl()   操作epoll对象，在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数

                3)epoll\_wait()  判断并完成对应的IO操作

            缺点：

                编程模型比select/poll 复杂

            例子：100万个连接，里面有1万个连接是活跃，在 select、poll、epoll分别是怎样的表现

                select：不修改宏定义，则需要 1000个进程才可以支持 100万连接

                poll：100万个链接，遍历都响应不过来了，还有空间的拷贝消耗大量的资源

                epoll:

 6、Java的I/O演进历史

    简介：讲解java的IO演进历史

    1、jdk1.4之前是采用同步阻塞模型，也就是BIO

        大型服务一般采用C或者C++, 因为可以直接操作系统提供的异步IO,AIO

    2、jdk1.4推出NIO,支持非阻塞IO，jdk1.7升级,推出NIO2.0,提供AIO的功能，支持文件和网络套接字的异步IO

7、大话Netty线程模型和Reactor模式

    简介：讲解reactor模式 和 Netty线程模型

    设计模式——Reactor模式（反应器设计模式），是一种基于事件驱动的设计模式，在事件驱动的应用中，将一个或多个客户的服务请求分离（demultiplex）和调度（dispatch）给应用程序。在事件驱动的应用中，同步地、有序地处理同时接收的多个服务请求

    一般出现在高并发系统中，比如Netty，Redis等

    优点

    1）响应快，不会因为单个同步而阻塞，虽然Reactor本身依然是同步的；

    2）编程相对简单，最大程度的避免复杂的多线程及同步问题，并且避免了多线程/进程的切换开销；

    3）可扩展性，可以方便的通过增加Reactor实例个数来充分利用CPU资源；

    缺点

    1）相比传统的简单模型，Reactor增加了一定的复杂性，因而有一定的门槛，并且不易于调试。

    2）Reactor模式需要系统底层的的支持，比如Java中的Selector支持，操作系统的select系统调用支持

    通俗理解：KTV例子 前台接待，服务人员带领去开机器

    Reactor模式基于事件驱动，适合处理海量的I/O事件，属于同步非阻塞IO(NIO)

    Reactor单线程模型(比较少用)

        内容：

        1）作为NIO服务端，接收客户端的TCP连接；作为NIO客户端，向服务端发起TCP连接；

        2）服务端读请求数据并响应；客户端写请求并读取响应

        使用场景:

            对应小业务则适合，编码简单；对于高负载、大并发的应用场景不适合，一个NIO线程处理太多请求，则负载过高，并且可能响应变慢，导致大量请求超时，而且万一线程挂了，则不可用了

    Reactor多线程模型

        内容：

        1）一个Acceptor线程，一组NIO线程，一般是使用自带的线程池，包含一个任务队列和多个可用的线程

        使用场景：

            可满足大多数场景，但是当Acceptor需要做复杂操作的时候，比如认证等耗时操作，再高并发情况下则也会有性能问题

    Reactor主从线程模型

        内容:

        1) Acceptor不在是一个线程，而是一组NIO线程；IO线程也是一组NIO线程，这样就是两个线程池去处理接入连接和处理IO

        使用场景：

        满足目前的大部分场景，也是Netty推荐使用的线程模型

        BossGroup

        WorkGroup

        附属资料：

            为什么Netty使用NIO而不是AIO，是同步非阻塞还是异步非阻塞？

            答案：

            在Linux系统上，AIO的底层实现仍使用EPOLL，与NIO相同，因此在性能上没有明显的优势

            Netty整体架构是reactor模型，采用epoll机制，所以往深的说，还是IO多路复用模式，所以也可说netty是同步非阻塞模型（看的层次不一样）

            很多人说这是netty是基于Java NIO 类库实现的异步通讯框架

            特点：异步非阻塞、基于事件驱动，性能高，高可靠性和高可定制性。

            参考资料：

            https://github.com/netty/netty/issues/2515

第四章：高并发网络编程Netty的第一个案例

    1、讲解什么是Echo服务和Netty项目搭建

        简介：讲解什么是Echo服务和快速创建Netty项目

            1)什么是Echo服务：就是一个应答服务（回显服务器），客户端发送什么数据，服务端就响应的对应的数据

            是一个非常有的用于调试和检测的服务

            2)IDEA + Maven + jdk8

                netty依赖包

            3) maven地址：https://mvnrepository.com/artifact/io.netty/netty-all/4.1.32.Final

    2、Netty实战之Echo服务-服务端程序编写实战

        简介：讲解Echo服务-服务端程序编写实战，对应的启动类和handler处理器

    3、Netty实战之Echo服务-客户端程序编写实战

        简介：讲解Echo服务客户端程序编写

4、Netty实战之Echo服务演示和整个流程分析

    简介：分析整个Echo服务各个组件名称和作用

        1）EventLoop和EventLoopGroup

        2) Bootstrapt启动引导类

        3）Channel 生命周期，状态变化

        4）ChannelHandler和ChannelPipline

第五章：Netty案例实战分析之核心链路源码讲解

    1、深入剖析EventLoop和EventLoopGroup线程模型

        简介：源码讲解EventLoop和EventLoopGroup模块

        1）高性能RPC框架的3个要素：IO模型(五种unix网络编程模型)、数据协议(http/protobuf/THRIFT)、线程模型(多路复用/传统请求线程一对一)

        2）EventLoop好比一个线程，1个EventLoop可以服务多个Channel，1个Channel只有一个EventLoop

        可以创建多个 EventLoop 来优化资源利用，也就是EventLoopGroup

        3）EventLoopGroup 负责分配 EventLoop 到新创建的 Channel，里面包含多个EventLoop

            EventLoopGroup -\> 多个 EventLoop

                EventLoop -\> 维护一个 Selector

            学习资料：http://ifeve.com/selectors/

        4）源码分析默认线程池数量

    2、Netty启动引导类Bootstrap模块讲解

        简介：讲解Netty启动引导类Bootstrap作用和tcp通道参数设置

            参考：https://blog.csdn.net/QH\_JAVA/article/details/78383543

        1）服务器启动引导类ServerBootstrap

                1) group :设置线程组模型，Reactor线程模型对比EventLoopGroup

                         1）单线程

                         2）多线程

                         3）主从线程

                        参考：https://blog.csdn.net/QH\_JAVA/article/details/78443646

                2）channel：设置channel通道类型NioServerSocketChannel、OioServerSocketChannel

                3) option: 作用于每个新建立的channel，设置TCP连接中的一些参数,如下

                            ChannelOption.SO\_BACKLOG: 存放已完成三次握手的请求的等待队列的最大长度;

                            Linux服务器TCP连接底层知识：

                                syn queue：半连接队列，洪水攻击，tcp\_max\_syn\_backlog

                                accept queue：全连接队列， net.core.somaxconn

                            系统默认的somaxconn参数要足够大 ，如果backlog比somaxconn大，则会优先用后者

                            https://github.com/netty/netty/blob/4.1/common/src/main/java/io/netty/util/NetUtil.java#L250

                            ChannelOption.TCP\_NODELAY: 为了解决Nagle的算法问题，默认是false, 要求高实时性，有数据时马上发送，就将该选项设置为true关闭Nagle算法；如果要减少发送次数，就设置为false，会累积一定大小后再发送；

                            知识拓展：https://baike.baidu.com/item/Nagle%E7%AE%97%E6%B3%95/5645172

                                    https://www.2cto.com/article/201309/241096.html

                4）childOption: 作用于被accept之后的连接

                5) childHandler: 用于对每个通道里面的数据处理

        2）客户端启动引导类Bootstrap

            1）remoteAddress： 服务端地址

            2）handler：和服务端通信的处理器

3、Netty核心组件Channel模块讲解

    简介:讲解Channel作用，核心模块知识点，生命周期等

        什么是Channel: 客户端和服务端建立的一个连接通道

        什么是ChannelHandler： 负责Channel的逻辑处理

        什么是ChannelPipeline: 负责管理ChannelHandler的有序容器

    他们是什么关系:

                一个Channel包含一个ChannelPipeline，所有ChannelHandler都会顺序加入到ChannelPipeline中

                创建Channel时会自动创建一个ChannelPipeline，每个Channel都有一个管理它的pipeline，这关联是永久性的

        Channel当状态出现变化，就会触发对应的事件

        状态：

            （1）channelRegistered: channel注册到一个EventLoop

            （2）channelActive: 变为活跃状态（连接到了远程主机），可以接受和发送数据

            （3）channelInactive: channel处于非活跃状态，没有连接到远程主机

            （4）channelUnregistered: channel已经创建，但是未注册到一个EventLoop里面，也就是没有和Selector绑定

    4、ChannelHandler和ChannelPipeline模块讲解

        简介：讲解ChannelHandler和ChannelPipeline核心作用和生命周期

        方法:

            handlerAdded : 当 ChannelHandler 添加到 ChannelPipeline 调用

            handlerRemoved : 当 ChannelHandler 从 ChannelPipeline 移除时调用

            exceptionCaught : 执行抛出异常时调用

        ChannelHandler下主要是两个子接口

                ChannelInboundHandler：(入站)

                    处理输入数据和Channel状态类型改变，

                    适配器 ChannelInboundHandlerAdapter（适配器设计模式）

                    常用的：SimpleChannelInboundHandler

                ChannelOutboundHandler：(出站)

                处理输出数据，适配器 ChannelOutboundHandlerAdapter

        ChannelPipeline：

        好比厂里的流水线一样，可以在上面添加多个ChannelHanler，也可看成是一串 ChannelHandler 实例，拦截穿过 Channel 的输入输出 event,  ChannelPipeline 实现了拦截器的一种高级形式，使得用户可以对事件的处理以及ChannelHanler之间交互获得完全的控制权

    5、Netty核心模块指ChannelHandlerContext模块讲解

        简介：讲解ChannelHandlerContext模块的作用和分析

            1、ChannelHandlerContext是连接ChannelHandler和ChannelPipeline的桥梁

            ChannelHandlerContext部分方法和Channel及ChannelPipeline重合,好比调用write方法，

            Channel、ChannelPipeline、ChannelHandlerContext 都可以调用此方法，前两者都会在整个管道流里传播，而ChannelHandlerContext就只会在后续的Handler里面传播

![](/Users/sunxu/Downloads/netty-projects-master/pic/Snipaste_2019-07-23_22-51-27.jpg)

            2、AbstractChannelHandlerContext类

                    双向链表结构，next/prev分别是后继节点，和前驱节点

            3、DefaultChannelHandlerContext 是实现类，但是大部分都是父类那边完成，这个只是简单的实现一些方法

                主要就是判断Handler的类型

                ChannelInboundHandler之间的传递，主要通过调用ctx里面的FireXXX()方法来实现下个handler的调用

    6、Netty案例实战常见问题之入站出站Handler执行顺序

        简介: 讲解多个入站出站ChannelHandler的执行顺序

            1、一般的项目中，inboundHandler和outboundHandler有多个，在Pipeline中的执行顺序？

                InboundHandler顺序执行，OutboundHandler逆序执行

                问题：ch.pipeline().addLast(new InboundHandler1());

                     ch.pipeline().addLast(new OutboundHandler1());

                     ch.pipeline().addLast(new OutboundHandler2());

                     ch.pipeline().addLast(new InboundHandler2());

                或者：

                     ch.pipeline().addLast(new OutboundHandler1());

                     ch.pipeline().addLast(new OutboundHandler2());

                     ch.pipeline().addLast(new InboundHandler1());

                     ch.pipeline().addLast(new InboundHandler2());

                 执行顺序是：

                    InboundHandler1 channelRead

                    InboundHandler2 channelRead

                    OutboundHandler2 write

                    OutboundHandler1 write

             结论：

                1）InboundHandler顺序执行，OutboundHandler逆序执行

                2）InboundHandler之间传递数据，通过ctx.fireChannelRead(msg)

                3）InboundHandler通过ctx.write(msg)，则会传递到outboundHandler

                4) 使用ctx.write(msg)传递消息，Inbound需要放在结尾，在Outbound之后，不然outboundhandler会不执行；

                    但是使用channel.write(msg)、pipline.write(msg)情况会不一致，都会执行

                5) outBound和Inbound谁先执行，针对客户端和服务端而言，客户端是发起请求再接受数据，先outbound再inbound，服务端则相反

    7、Netty异步操作模块ChannelFuture讲解

            简介：讲解ChannelFuture异步操作模块及使用注意事项

    Netty中的所有I/O操作都是异步的,这意味着任何I/O调用都会立即返回，而ChannelFuture会提供有关的信息I/O操作的结果或状态。

        1）ChannelFuture状态：

            未完成：当I/O操作开始时，将创建一个新的对象，新的最初是未完成的 - 它既没有成功，也没有成功，也没有被取消，因为I/O操作尚未完成。

            已完成：当I/O操作完成，不管是成功、失败还是取消，Future都是标记为已完成的, 失败的时候也有具体的信息，例如原因失败，但请注意，即使失败和取消属于完成状态。

            注意：不要在IO线程内调用future对象的sync或者await方法

                不能在channelHandler中调用sync或者await方法

        2）ChannelPromise：继承于ChannelFuture，进一步拓展用于设置IO操作的结果

第六章：高并发架构之Netty网络数据传输编解码精讲

    1、Netty网络传输知识之什么是编码、解码

            简介：讲解Netty编写的网络数据传输中的编码和解码

            前面说的：高性能RPC框架的3个要素：IO模型、数据协议、线程模型

            最开始接触的编码码：java序列化/反序列化（就是编解码）、url编码、base64编解码

            为啥jdk有编解码，还要netty自己开发编解码？

                java自带序列化的缺点

                    1）无法跨语言

                    2) 序列化后的码流太大，也就是数据包太大

                    3) 序列化和反序列化性能比较差

            业界里面也有其他编码框架： google的 protobuf（PB)、Facebook的Trift、Jboss的Marshalling、Kyro等

            Netty里面的编解码：

                解码器：负责处理"入站 InboundHandler"数据

                编码器：负责"出站 OutboundHandler" 数据

                Netty里面提供默认的编解码器，也支持自定义编解码器

                Encoder:编码器

                Decoder:解码器

                Codec:编解码器

 2、数据协议处理之Netty解码器Decoder讲解

    简介:讲解Netty的解码器Decoder和使用场景

        Decoder对应的就是ChannelInboundHandler，主要就是字节数组转换为消息对象

        主要是两个方法

            decode

            decodeLast

        抽象解码器

            1)ByteToMessageDecoder

                用于将字节转为消息，需要检查缓冲区是否有足够的字节

            2)ReplayingDecoder

                继承ByteToMessageDecoder，不需要检查缓冲区是否有足够的字节，但是ReplayingDecoder速度略满于ByteToMessageDecoder，不是所有的ByteBuf都支持

            选择：项目复杂性高则使用ReplayingDecoder，否则使用 ByteToMessageDecoder

            3）MessageToMessageDecoder

                用于从一种消息解码为另外一种消息（例如POJO到POJO）

        解码器具体的实现，用的比较多的是(更多是为了解决TCP底层的粘包和拆包问题)

            DelimiterBasedFrameDecoder： 指定消息分隔符的解码器

            LineBasedFrameDecoder: 以换行符为结束标志的解码器

            FixedLengthFrameDecoder：固定长度解码器

            LengthFieldBasedFrameDecoder：message = header+body, 基于长度解码的通用解码器

            StringDecoder：文本解码器，将接收到的对象转化为字符串，一般会与上面的进行配合，然后在后面添加业务handle

3、数据协议处理之Netty编码器Encoder讲解

    简介：讲解Netty编码器Encoder

        Encoder对应的就是ChannelOutboundHandler，消息对象转换为字节数组

        Netty本身未提供和解码一样的编码器，是因为场景不同，两者非对等的

        1)MessageToByteEncoder

            消息转为字节数组,调用write方法，会先判断当前编码器是否支持需要发送的消息类型，如果不支持，则透传；

        2)MessageToMessageEncoder

            用于从一种消息编码为另外一种消息（例如POJO到POJO）

4、数据协议处理之Netty编解码器类Codec讲解

    简介：讲解组合编解码器类Codec

        组合解码器和编码器，以此提供对于字节和消息都相同的操作

        优点：成对出现，编解码都是在一个类里面完成

        缺点：耦合在一起，拓展性不佳

        Codec:组合编解码

            1）ByteToMessageCodec

            2）MessageToMessageCodec

        decoder:解码

             1）ByteToMessageDecoder

             2）MessageToMessageDecoder

        encoder:编码

             1）ByteToMessageEncoder

            2）MessageToMessageEncoder

第七章、Netty核心知识之网络传输TCP粘包拆包

    1、网络编程核心知识之TCP粘包拆包讲解

        简介：讲解什么是TCP粘包拆包讲解

        1）TCP拆包: 一个完整的包可能会被TCP拆分为多个包进行发送

        2）TCP粘包: 把多个小的包封装成一个大的数据包发送, client发送的若干数据包 Server接收时粘成一包

       发送方和接收方都可能出现这个原因

        发送方的原因：TCP默认会使用Nagle算法

        接收方的原因: TCP接收到数据放置缓存中，应用程序从缓存中读取

       UDP: 是没有粘包和拆包的问题，有边界协议

    2、TCP半包读写常见解决方案

        简介：讲解TCP半包读写常见的解决办法

             发送方：可以关闭Nagle算法

             接受方: TCP是无界的数据流，并没有处理粘包现象的机制, 且协议本身无法避免粘包，半包读写的发生需要在应用层进行处理

                    应用层解决半包读写的办法

                        1）设置定长消息 (10字符)

                            xdclass000xdclass000xdclass000xdclass000

                        2）设置消息的边界 ($$ 切割)

                            sdfafwefqwefwe$$dsafadfadsfwqehidwuehfiw$$879329832r89qweew$$

                        3）使用带消息头的协议，消息头存储消息开始标识及消息的长度信息

                                Header+Body

    3、Netty自带解决TCP半包读写方案

        简介：讲解Netty自带解决半包读写问题方案介绍

            DelimiterBasedFrameDecoder： 指定消息分隔符的解码器

            LineBasedFrameDecoder: 以换行符为结束标志的解码器

            FixedLengthFrameDecoder：固定长度解码器

            LengthFieldBasedFrameDecoder：message = header+body, 基于长度解码的通用解码器

    4、Netty案例实战之半包读写问题演示

        简介：案例实战之使用netty进行开发，出现的TCP半包读写问题

    5、Netty案例实战之LineBasedFrameDecoder解决TCP半包读写

        简介：讲解使用解码器LineBasedFrameDecoder解决半包读写问题

            1）LineBaseFrameDecoder 以换行符为结束标志的解码器 ,构造函数里面的数字表示最长遍历的帧数

            2）StringDecoder解码器将对象转成字符串

    6、Netty案例实战之自定义分隔符解决TCP读写问题

        简介：讲解使用DelimiterBasedFrameDecoder解决TCP半包读写问题

            maxLength：

                表示一行最大的长度，如果超过这个长度依然没有检测自定义分隔符，将会抛出TooLongFrameException

            failFast：

                如果为true，则超出maxLength后立即抛出TooLongFrameException，不进行继续解码

                如果为false，则等到完整的消息被解码后，再抛出TooLongFrameException异常

            stripDelimiter：

                解码后的消息是否去除掉分隔符

            delimiters：

                分隔符，ByteBuf类型

    7、自定义长度半包读写器LengthFieldBasedFrameDecoder讲解

     简介：自定义长度半包读写器LengthFieldBasedFrameDecoder讲解

     官方文档：https://netty.io/4.0/api/io/netty/handler/codec/LengthFieldBasedFrameDecoder.html

     maxFrameLength 数据包的最大长度

     lengthFieldOffset 长度字段的偏移位，长度字段开始的地方，意思是跳过指定长度个字节之后的才是消息体字段

     lengthFieldLength 长度字段占的字节数, 帧数据长度的字段本身的长度

     lengthAdjustment

     一般 Header + Body，添加到长度字段的补偿值,如果为负数，开发人员认为这个 Header的长度字段是整个消息包的长度，则Netty应该减去对应的数字

     initialBytesToStrip 从解码帧中第一次去除的字节数, 获取完一个完整的数据包之后，忽略前面的指定位数的长度字节，应用解码器拿到的就是不带长度域的数据包

     failFast 是否快速失败

第八 Netty源码分析之基础数据传输讲解和设计模式

    1、Netty核心模块缓冲ByteBuf

        简介：讲解Netty核心之ByteBuf介绍，对比JDK原生ByteBuffer

        ByteBuf：是数据容器(字节容器)

        JDK ByteBuffer

         共用读写索引，每次读写操作都需要Flip()

         扩容麻烦，而且扩容后容易造成浪费

        Netty ByteBuf

         读写使用不同的索引，所以操作便捷

         自动扩容，使用便捷

    2、Netty数据存储模块ByteBuf创建方法和常用的模式

        简介:讲解ByteBuf创建方法和常用的模式

         ByteBuf：传递字节数据的容器

         ByteBuf的创建方法

          1）ByteBufAllocator

            池化(Netty4.x版本后默认使用 PooledByteBufAllocator

             提高性能并且最大程度减少内存碎片

            非池化UnpooledByteBufAllocator： 每次返回新的实例

          2）Unpooled: 提供静态方法创建未池化的ByteBuf，可以创建堆内存和直接内存缓冲区

         ByteBuf使用模式

          堆缓存区HEAP BUFFER:

           优点：存储在JVM的堆空间中，可以快速的分配和释放

           缺点：每次使用前会拷贝到直接缓存区(也叫堆外内存)

          直接缓存区DIRECR BUFFER:

           优点：存储在堆外内存上，堆外分配的直接内存，不会占用堆空间

           缺点：内存的分配和释放，比在堆缓冲区更复杂

          复合缓冲区COMPOSITE BUFFER:

           可以创建多个不同的ByteBuf，然后放在一起，但是只是一个视图

          选择：大量IO数据读写，用"直接缓存区"； 业务消息编解码用"堆缓存区"

  3、Netty里面的设计模式应用分析

   简介：讲解设计模式的在Netty里面的应用

    Builder构造器模式：ServerBootstap

    责任链设计模式：pipeline的事件传播

    工厂模式： 创建Channel

    适配器模式：HandlerAdapter

   推荐书籍：《Head First设计模式》

第九章 使用Netty搭建单机百万连接测试实战

1、搭建单机百万连接的服务器实例的必备知识

    简介：搭建单机百万连接的服务器实例的必备知识

    1、网络IO模型

    2、Linux文件描述符

        单进程文件句柄数(默认1024，不同系统不一样，每个进程都有最大的文件描述符限制)

        全局文件句柄数

    3、如何确定一个唯一的TCP连接

        TCP四元组:源IP地址、源端口、目的ip、目的端口

2、Netty单机百万连接实战之服务端代码编写

    简介：讲解Netty单机百万连接服务端代码编写

3、Netty单机百万连接实战之客户端代码编写

    简介：讲解Netty单机百万连接之客户端代码编写

4、阿里云服务器Netty单机百万连接部署实战

    简介：在阿里云服务器部署Netty服务端和Netty客户端代码

    （如果没条件，则自己搭建虚拟机 6G，4核，centos6.5/7,需要关闭防火墙，或者使用云服务器需要开放安全组）

     服务端 172.16.17.32

     客户端 172.16.17.32

     windows: wscp图形界面，putty终端

5、Netty单机百万连接Linux内核参数优化

    简介：单机百万连接Linux核心参数优化

        局部文件句柄限制（单个进程最大文件打开数）

        ulimit -n 一个进程最大打开的文件数 fd 不同系统有不同的默认值

        root身份编辑   vim /etc/security/limits.conf

                增加下面

                root soft nofile 1000000

                root hard nofile 1000000

                \* soft nofile 1000000

                \* hard nofile 1000000

        \* 表示当前用户，修改后要重启

        全局文件句柄限制（所有进程最大打开的文件数，不同系统是不一样，可以直接echo临时修改）

        查看命令

            cat /proc/sys/fs/file-max

            vim  /etc/sysctl.conf

            增加 fs.file-max = 1000000
             永久修改全局文件句柄, 修改后生效 sysctl -p

        启动

            java -jar millionServer-1.0-SNAPSHOT.jar  -Xms5g -Xmx5g -XX:NewSize=3g -XX:MaxNewSize=3g

6、互联网架构数据链路分析总结

    简介：讲解当下互联网架构中，数据链路分析总结

        输入域名-》浏览器内核调度-》本地DNS解析-》远程DNS解析-》ip -》路由多层调转-》目的服务器

        服务器内核-》代理服务器 nginx/ 网关 / 负载均衡设备-》目的服务器

        服务器内核-》 应用程序（springboot）-》Redis-》Mysql

第十章：高并发系列之百万连接Netty实战课程总结

    1、高并发系列之百万连接Netty实战课程总结

        简介：总结Netty实战课程和第二季展望

         websocket

         推送系统

         RPC框架

<file_sep>/notes/Kafka.md

- [基本概念](#基本概念)
    - [Broker](#broker)
    - [Topic](#topic)
    - [Partition](#partition)
    - [Consumer Group 即 User Group](#consumer-group-即-user-group)
    - [Offset](#offset)
- [Kafka和RocketMq的不同](#kafka和rocketmq的不同)
- [kafka消息的同步机制](#kafka消息的同步机制)
    - [Producer的三种保证](#producer的三种保证)
    - [Consumer的三种保证](#consumer的三种保证)
    - [消息传递过程](#消息传递过程)
- [Kafka的offset与提交关系](#kafka的offset与提交关系)
    - [自动提交](#自动提交)
    - [手动提交](#手动提交)
        - [手动提交的三种方式 同步 异步 混合(针对提交失败的处理方式)](#手动提交的三种方式-同步-异步-混合针对提交失败的处理方式)

# 基本概念

同一个topic的消息,有多个分区,会被平均分配(不是复制)到这几个分区,
同一个组的多个消费者,会分别负责一个分区,来取他们中的消息消费;如果是多个消费者负责一个分区,会有offset来控制消费顺序.

该图中只有一个topic ,该topic下有t0,t1,t2,t3四个消息.

如果还有第二个topic,topic下的消息有t8,t9,t10,t11.那么跟t0123的分配方式是一样的.

## Broker
物理概念，Kafka集群包含一个或多个服务器，这种服务器被称为broker。

## Topic
是Kafka下消息的类别，类似于RabbitMQ中的Exchange的概念。这是逻辑上的概念，用来区分、隔离不同的消息数据，屏蔽了底层复杂的存储方式。对于大多数人来说，在开发的时候只需要关注数据写入到了哪个topic、从哪个topic取出数据。

## Partition
是Kafka下数据存储的基本单元，这个是物理上的概念。**同一个topic的数据，会被分散的存储到多个partition中**，其优势在于：有利于水平扩展，避免单台机器在磁盘空间和性能上的限制，为了做到均匀分布，通常partition的数量通常是Broker Server数量的整数倍。

## Consumer Group 即 User Group
是Kafka实现单播和广播两种消息模型的手段。同一个topic的数据，会广播给不同的group；同一个group中的worker，只有一个worker能拿到这个数据。换句话说，对于同一个topic，每个group都可以拿到同样的所有数据，但是数据进入group后只能被其中的一个worker消费。

group内的worker可以使用多线程或多进程来实现，也可以将进程分散在多台机器上，**worker的数量通常不超过partition的数量，且二者最好保持整数倍关系**，因为Kafka在设计时假定了一个partition只能被一个worker消费（同一group内）。User group是为了便于实现MQ中的多播，重复消费等引入的概念。如果ConsumerA以及ConsumerB同在一个UserGroup，那么ConsumerA消费的数据ConsumerB就无法消费了。usergroup中的consumer使用一套offset。组内必然可以有多个消费者或消费者实例(consumer instance)，它们共享一个公共的ID，即group ID。

组内的所有消费者协调在一起来消费订阅主题(subscribed topics)的所有分区(partition)。当然，每个分区只能由同一个消费组内的一个consumer来消费。
1）consumer group下可以有一个或多个consumer instance，consumer instance可以是一个进程，也可以是一个线程
2）group.id是一个字符串，唯一标识一个consumer group
3）consumer group下订阅的topic下的每个分区只能分配给某个group下的一个consumer(当然该分区还可以被分配给其他group)

**一个topic有多个分区,每个group可以订阅多个topic,group中的 每个consumer instance都可以消费同一个topic下每个分区的数据,但是group存有每个分区的offset,所以保证一个topic 多个分区下的每个数据都只被 一个group 消费一次**

## Offset
Offset专指Partition以及User Group而言，记录某个user group在某个partiton中当前已经消费到达的位置。

# Kafka和RocketMq的不同

https://www.jianshu.com/p/c474ca9f9430

# kafka消息的同步机制

producer与consumer的delivery guarantee有三种：

At most once 消息可能会丢，但绝不会重复传输

At least one 消息绝不会丢，但可能会重复传输

Exactly once 每条消息肯定会被传输一次且仅传输一次，很多时候这是用户所想要的

## Producer的三种保证

producer 的deliver guarantee 可以通过request.required.acks参数的设置来进行调整：

0 ，相当于异步发送，消息发送完毕即offset增加，继续生产；相当于At most once

1，leader收到leader replica 对一个消息的接受ack才增加offset，然后继续生产；

-1，leader收到所有replica 对一个消息的接受ack才增加offset，然后继续生产

当producer向broker发送消息时，一旦这条消息被commit，因数replication的存在，它就不会丢。但是如果producer发送数据给broker后，遇到的网络问题而造成通信中断，那producer就无法判断该条消息是否已经commit。这一点有点像向一个自动生成primary key的数据库表中插入数据。虽然Kafka无法确定网络故障期间发生了什么，但是producer可以生成一种类似于primary key的东西，发生故障时幂等性的retry多次，这样就做到了Exactly one。截止到目前(Kafka 0.8.2版本，2015-01-25)，这一feature还并未实现，有希望在Kafka未来的版本中实现。（所以目前默认情况下一条消息从producer和broker是确保了At least once，但可通过设置producer异步发送实现At most once）。

## Consumer的三种保证

　　consumer在从broker读取消息后，可以选择commit，该操作会在Zookeeper中存下该consumer在该partition下读取的消息的offset。该consumer下一次再读该partition时会从下一条开始读取。如未commit，下一次读取的开始位置会跟上一次commit之后的开始位置相同。当然可以将consumer设置为autocommit，即consumer一旦读到数据立即自动commit。如果只讨论这一读取消息的过程，那Kafka是确保了Exactly once。但实际上实际使用中consumer并非读取完数据就结束了，而是要进行进一步处理，而数据处理与commit的顺序在很大程度上决定了消息从broker和consumer的delivery guarantee semantic。

读完消息先commit再处理消息。这种模式下，如果consumer在commit后还没来得及处理消息就crash了，下次重新开始工作后就无法读到刚刚已提交而未处理的消息，这就对应于At most once

读完消息先处理再commit。这种模式下，如果处理完了消息在commit之前consumer crash了，下次重新开始工作时还会处理刚刚未commit的消息，实际上该消息已经被处理过了。这就对应于At least once（默认）

如果一定要做到Exactly once，就需要协调offset和实际操作的输出。精典的做法是引入两阶段提交。如果能让offset和操作输入存在同一个地方，会更简洁和通用。这种方式可能更好，因为许多输出系统可能不支持两阶段提交。比如，consumer拿到数据后可能把数据放到HDFS，如果把最新的offset和数据本身一起写到HDFS，那就可以保证数据的输出和offset的更新要么都完成，要么都不完成，间接实现Exactly once。（目前就high level API而言，offset是存于Zookeeper中的，无法存于HDFS，而low level API的offset是由自己去维护的，可以将之存于HDFS中）

## 消息传递过程

　　Producer在发布消息到某个Partition时，先通过Zookeeper找到该Partition的Leader，然后无论该Topic的Replication Factor为多少（也即该Partition有多少个Replica），Producer只将该消息发送到该Partition的Leader。Leader会将该消息写入其本地Log。每个Follower都从Leader pull数据。这种方式上，Follower存储的数据顺序与Leader保持一致。Follower在收到该消息并写入其Log后，向Leader发送ACK。一旦Leader收到了ISR中的所有Replica的ACK，该消息就被认为已经commit了，Leader将增加HW（即offset）并且向Producer发送ACK。

为了提高性能，每个Follower在接收到数据后就立马向Leader发送ACK，而非等到数据写入Log中。因此，对于已经commit的消息，Kafka只能保证它被存于多个Replica的内存中，而不能保证它们被持久化到磁盘中，也就不能完全保证异常发生后该条消息一定能被Consumer消费。但考虑到这种场景非常少见，可以认为这种方式在性能和数据持久化上做了一个比较好的平衡。在将来的版本中，Kafka会考虑提供更高的持久性。

Consumer读消息也是从Leader读取，只有被commit过的消息（offset低于HW的消息）才会暴露给Consumer。

# Kafka的offset与提交关系

//设置不自动提交，手动更新offset:`properties.put("enable.auto.commit", "false");`

## 自动提交

Kafka中偏移量的自动提交是由参数enable_auto_commit和auto_commit_interval_ms控制的，当enable_auto_commit=True时，Kafka在消费的过程中会以频率为auto_commit_interval_ms向Kafka自带的topic(__consumer_offsets)进行偏移量提交，具体提交到哪个Partation是以算法：partation=hash(group_id)%50来计算的。

调用consumer.close()时候也会触发自动提交，因为它默认autocommit=True

对于自动提交偏移量，如果auto_commit_interval_ms的值设置的过大，当消费者在自动提交偏移量之前异常退出，将导致kafka未提交偏移量，进而出现重复消费的问题，所以建议auto_commit_interval_ms的值越小越好。

如果让kafka自动去维护offset，消费者拉到消息还没消费完成,它就会认为这条数据已经被消费了，那么会造成**数据丢失**.

## 手动提交

[关于Kafka 的 consumer 消费者手动提交详解](https://www.cnblogs.com/xuwujing/p/8432984.html)

consumer端手动提交offset之后,该offset之前的消息都会被当成已经消费,之后的数据即使消费了也会被当成没消费.比如:服务端有10条数据,consumer消费到第5条,commit了,消费到第10条没有commit程序退出,下次consumer重启之后,会从第6条开始消费.

如果让消费者手动提交，如果在上面的场景中，那么需要我们手动commit，如果comsumer挂了,程序就不会执行commit,其他同group的消费者又可以消费这条数据，保证数据不丢.但可能会导致**重复消费**,

### 手动提交的三种方式 同步 异步 混合(针对提交失败的处理方式)

[Kafka提交offset机制](https://www.cnblogs.com/FG123/p/10091599.html)

1.同步手动提交偏移量

同步模式下提交失败的时候一直尝试提交，直到遇到无法重试的情况下才会结束，同时同步方式下消费者线程在拉取消息会被阻塞，在broker对提交的请求做出响应之前，会一直阻塞直到偏移量提交操作成功或者在提交过程中发生异常，限制了消息的吞吐量。

consumer.commit()

2.异步手动提交偏移量+回调函数

 异步手动提交offset时，消费者线程不会阻塞，提交失败的时候也不会进行重试，并且可以配合回调函数在broker做出响应的时候记录错误信息。

对于异步提交，由于不会进行失败重试，当消费者异常关闭或者触发了再均衡前，如果偏移量还未提交就会造成偏移量丢失。

consumer.commit_async(callback=_on_send_response)

 3.异步+同步 组合的方式提交偏移量

针对异步提交偏移量丢失的问题，通过对消费者进行异步批次提交并且在关闭时同步提交的方式，这样即使上一次的异步提交失败，通过同步提交还能够进行补救，同步会一直重试，直到提交成功。

通过finally在最后不管是否异常都会触发consumer.commit()来同步补救一次，确保偏移量不会丢失

```python
"""
同步和异步组合的方式提交偏移量
"""

import pickle
import uuid
import time
from kafka import KafkaConsumer

consumer = KafkaConsumer(
    bootstrap_servers=['192.168.33.11:9092'],
    group_id="test_group_1",
    client_id="{}".format(str(uuid.uuid4())),
    enable_auto_commit=False,  # 设置为手动提交偏移量.
    key_deserializer=lambda k: pickle.loads(k),
    value_deserializer=lambda v: pickle.loads(v)
)

# 订阅消费round_topic这个主题
consumer.subscribe(topics=('round_topic',))

def _on_send_response(*args, **kwargs):
    """
    提交偏移量涉及的回调函数
    :param args:
    :param kwargs:
    :return:
    """
    if isinstance(args[1], Exception):
        print('偏移量提交异常. {}'.format(args[1]))
    else:
        print('偏移量提交成功')

try:
    start_time = time.time()
    while True:
        consumer_records_dict = consumer.poll(timeout_ms=100)

        record_num = 0
        for key, record_list in consumer_records_dict.items():
            for record in record_list:
                record_num += 1
        print("---->当前批次获取到的消息个数是:<----".format(record_num))
        record_num = 0

        for k, record_list in consumer_records_dict.items():
            print(k)
            for record in record_list:
                print("topic = {},partition = {},offset = {},key = {},value = {}".format(
                    record.topic, record.partition, record.offset, record.key, record.value)
                )

        try:
            # 轮询一个batch 手动提交一次
            consumer.commit_async(callback=_on_send_response)
            end_time = time.time()
            time_counts = end_time - start_time
            print(time_counts)
        except Exception as e:
            print('commit failed', str(e))

except Exception as e:
    print(str(e))
finally:
    try:
        # 同步提交偏移量,在消费者异常退出的时候再次提交偏移量,确保偏移量的提交.
        consumer.commit()
        print("同步补救提交成功")
    except Exception as e:
        consumer.close()
```

<file_sep>/notes/Spark.md

- [Spark基础](#spark%e5%9f%ba%e7%a1%80)
  - [spark来历](#spark%e6%9d%a5%e5%8e%86)
  - [Spark内置模块](#spark%e5%86%85%e7%bd%ae%e6%a8%a1%e5%9d%97)
  - [Spark特点](#spark%e7%89%b9%e7%82%b9)
  - [Driver与Excetor](#driver%e4%b8%8eexcetor)
    - [Driver（驱动器）](#driver%e9%a9%b1%e5%8a%a8%e5%99%a8)
    - [Executor（执行器）](#executor%e6%89%a7%e8%a1%8c%e5%99%a8)
  - [WordCount思路](#wordcount%e6%80%9d%e8%b7%af)
  - [Spark在Yarn下运行](#spark%e5%9c%a8yarn%e4%b8%8b%e8%bf%90%e8%a1%8c)
  - [使用yarn](#%e4%bd%bf%e7%94%a8yarn)
  - [原理](#%e5%8e%9f%e7%90%86)
    - [Yarn的架构](#yarn%e7%9a%84%e6%9e%b6%e6%9e%84)
    - [Spark向Yarn提交任务的过程](#spark%e5%90%91yarn%e6%8f%90%e4%ba%a4%e4%bb%bb%e5%8a%a1%e7%9a%84%e8%bf%87%e7%a8%8b)
- [RDD](#rdd)
  - [JavaIO](#javaio)
    - [字符流的读取](#%e5%ad%97%e7%ac%a6%e6%b5%81%e7%9a%84%e8%af%bb%e5%8f%96)
  - [RDD介绍](#rdd%e4%bb%8b%e7%bb%8d)
    - [RDD的原理](#rdd%e7%9a%84%e5%8e%9f%e7%90%86)
  - [RDD 的属性](#rdd-%e7%9a%84%e5%b1%9e%e6%80%a7)
  - [RDD的特点](#rdd%e7%9a%84%e7%89%b9%e7%82%b9)
    - [分区](#%e5%88%86%e5%8c%ba)
    - [只读](#%e5%8f%aa%e8%af%bb)
    - [依赖](#%e4%be%9d%e8%b5%96)
    - [缓存](#%e7%bc%93%e5%ad%98)
    - [CheckPoint](#checkpoint)
    - [依赖关系](#%e4%be%9d%e8%b5%96%e5%85%b3%e7%b3%bb)
  - [RDD编程](#rdd%e7%bc%96%e7%a8%8b)
    - [RDD创建](#rdd%e5%88%9b%e5%bb%ba)
      - [从集合创建](#%e4%bb%8e%e9%9b%86%e5%90%88%e5%88%9b%e5%bb%ba)
      - [从外部存储系统创建](#%e4%bb%8e%e5%a4%96%e9%83%a8%e5%ad%98%e5%82%a8%e7%b3%bb%e7%bb%9f%e5%88%9b%e5%bb%ba)
    - [分区原理](#%e5%88%86%e5%8c%ba%e5%8e%9f%e7%90%86)
  - [RDD的转换](#rdd%e7%9a%84%e8%bd%ac%e6%8d%a2)
    - [Value类型](#value%e7%b1%bb%e5%9e%8b)
      - [map(func)案例](#mapfunc%e6%a1%88%e4%be%8b)
      - [mapPartitions(func) 案例](#mappartitionsfunc-%e6%a1%88%e4%be%8b)
      - [mapPartitionsWithIndex(func) 案例](#mappartitionswithindexfunc-%e6%a1%88%e4%be%8b)
    - [Driver与Excetor之间数据的传递](#driver%e4%b8%8eexcetor%e4%b9%8b%e9%97%b4%e6%95%b0%e6%8d%ae%e7%9a%84%e4%bc%a0%e9%80%92)
      - [latMap(func) 案例](#latmapfunc-%e6%a1%88%e4%be%8b)
      - [map()和mapPartition()的区别](#map%e5%92%8cmappartition%e7%9a%84%e5%8c%ba%e5%88%ab)
      - [glom案例](#glom%e6%a1%88%e4%be%8b)
      - [groupBy(func)案例](#groupbyfunc%e6%a1%88%e4%be%8b)
      - [filter(func) 案例](#filterfunc-%e6%a1%88%e4%be%8b)
      - [sample(withReplacement, fraction, seed) 案例](#samplewithreplacement-fraction-seed-%e6%a1%88%e4%be%8b)
      - [distinct([numTasks])) 案例](#distinctnumtasks-%e6%a1%88%e4%be%8b)
      - [coalesce(numPartitions) 案例](#coalescenumpartitions-%e6%a1%88%e4%be%8b)
      - [repartition(numPartitions) 案例](#repartitionnumpartitions-%e6%a1%88%e4%be%8b)
      - [coalesce和repartition的区别](#coalesce%e5%92%8crepartition%e7%9a%84%e5%8c%ba%e5%88%ab)
      - [sortBy(func,[ascending], [numTasks]) 案例](#sortbyfuncascending-numtasks-%e6%a1%88%e4%be%8b)
      - [pipe(command, [envVars]) 案例](#pipecommand-envvars-%e6%a1%88%e4%be%8b)
    - [双Value类型交互](#%e5%8f%8cvalue%e7%b1%bb%e5%9e%8b%e4%ba%a4%e4%ba%92)
      - [2.3.2.1 union(otherDataset) 案例](#2321-unionotherdataset-%e6%a1%88%e4%be%8b)
      - [2.3.2.2 subtract (otherDataset) 案例](#2322-subtract-otherdataset-%e6%a1%88%e4%be%8b)
      - [2.3.2.3 intersection(otherDataset) 案例](#2323-intersectionotherdataset-%e6%a1%88%e4%be%8b)
      - [2.3.2.4 cartesian(otherDataset) 案例](#2324-cartesianotherdataset-%e6%a1%88%e4%be%8b)
      - [2.3.2.5 zip(otherDataset)案例](#2325-zipotherdataset%e6%a1%88%e4%be%8b)
    - [2.3.3 Key-Value类型](#233-key-value%e7%b1%bb%e5%9e%8b)
      - [2.3.3.1 partitionBy案例](#2331-partitionby%e6%a1%88%e4%be%8b)
      - [2.3.3.2 groupByKey案例](#2332-groupbykey%e6%a1%88%e4%be%8b)
      - [2.3.3.3 reduceByKey(func, [numTasks]) 案例](#2333-reducebykeyfunc-numtasks-%e6%a1%88%e4%be%8b)
      - [2.3.3.4 reduceByKey和groupByKey的区别](#2334-reducebykey%e5%92%8cgroupbykey%e7%9a%84%e5%8c%ba%e5%88%ab)
      - [2.3.3.5 aggregateByKey案例](#2335-aggregatebykey%e6%a1%88%e4%be%8b)
      - [2.3.3.6 foldByKey案例](#2336-foldbykey%e6%a1%88%e4%be%8b)
      - [2.3.3.7 combineByKey[C] 案例](#2337-combinebykeyc-%e6%a1%88%e4%be%8b)
      - [2.3.3.8 sortByKey([ascending], [numTasks]) 案例](#2338-sortbykeyascending-numtasks-%e6%a1%88%e4%be%8b)
      - [2.3.3.9 mapValues案例](#2339-mapvalues%e6%a1%88%e4%be%8b)
      - [2.3.3.10 join(otherDataset, [numTasks]) 案例](#23310-joinotherdataset-numtasks-%e6%a1%88%e4%be%8b)
      - [2.3.3.11 cogroup(otherDataset, [numTasks]) 案例](#23311-cogroupotherdataset-numtasks-%e6%a1%88%e4%be%8b)
    - [2.3.4 案例实操](#234-%e6%a1%88%e4%be%8b%e5%ae%9e%e6%93%8d)
  - [2.4 Action](#24-action)
    - [2.4.1 reduce(func)案例](#241-reducefunc%e6%a1%88%e4%be%8b)
    - [2.4.2 collect()案例](#242-collect%e6%a1%88%e4%be%8b)
    - [2.4.3 count()案例](#243-count%e6%a1%88%e4%be%8b)
    - [2.4.4 first()案例](#244-first%e6%a1%88%e4%be%8b)
    - [2.4.5 take(n)案例](#245-taken%e6%a1%88%e4%be%8b)
    - [2.4.6 takeOrdered(n)案例](#246-takeorderedn%e6%a1%88%e4%be%8b)
    - [2.4.7 aggregate案例](#247-aggregate%e6%a1%88%e4%be%8b)
    - [2.4.8 fold(num)(func)案例](#248-foldnumfunc%e6%a1%88%e4%be%8b)
    - [2.4.9 saveAsTextFile(path)](#249-saveastextfilepath)
    - [2.4.10 saveAsSequenceFile(path)](#2410-saveassequencefilepath)
    - [2.4.11 saveAsObjectFile(path)](#2411-saveasobjectfilepath)
    - [2.4.12 countByKey()案例](#2412-countbykey%e6%a1%88%e4%be%8b)
    - [2.4.13 foreach(func)案例](#2413-foreachfunc%e6%a1%88%e4%be%8b)
  - [RDD中的函数传递](#rdd%e4%b8%ad%e7%9a%84%e5%87%bd%e6%95%b0%e4%bc%a0%e9%80%92)
    - [传递一个方法](#%e4%bc%a0%e9%80%92%e4%b8%80%e4%b8%aa%e6%96%b9%e6%b3%95)
    - [2.5.2 传递一个属性](#252-%e4%bc%a0%e9%80%92%e4%b8%80%e4%b8%aa%e5%b1%9e%e6%80%a7)
  - [RDD依赖关系](#rdd%e4%be%9d%e8%b5%96%e5%85%b3%e7%b3%bb)
    - [Lineage](#lineage)
    - [窄依赖](#%e7%aa%84%e4%be%9d%e8%b5%96)
    - [宽依赖](#%e5%ae%bd%e4%be%9d%e8%b5%96)
    - [DAG](#dag)
    - [任务划分（面试重点）](#%e4%bb%bb%e5%8a%a1%e5%88%92%e5%88%86%e9%9d%a2%e8%af%95%e9%87%8d%e7%82%b9)
  - [RDD缓存](#rdd%e7%bc%93%e5%ad%98)
    - [RDD CheckPoint](#rdd-checkpoint)
  - [键值对RDD数据分区器](#%e9%94%ae%e5%80%bc%e5%af%b9rdd%e6%95%b0%e6%8d%ae%e5%88%86%e5%8c%ba%e5%99%a8)
    - [Hash分区](#hash%e5%88%86%e5%8c%ba)
    - [Ranger分区](#ranger%e5%88%86%e5%8c%ba)
    - [自定义分区](#%e8%87%aa%e5%ae%9a%e4%b9%89%e5%88%86%e5%8c%ba)
  - [数据读取与保存](#%e6%95%b0%e6%8d%ae%e8%af%bb%e5%8f%96%e4%b8%8e%e4%bf%9d%e5%ad%98)
    - [文件类数据读取与保存](#%e6%96%87%e4%bb%b6%e7%b1%bb%e6%95%b0%e6%8d%ae%e8%af%bb%e5%8f%96%e4%b8%8e%e4%bf%9d%e5%ad%98)
      - [Text文件](#text%e6%96%87%e4%bb%b6)
      - [Json文件](#json%e6%96%87%e4%bb%b6)
      - [对象文件](#%e5%af%b9%e8%b1%a1%e6%96%87%e4%bb%b6)
    - [文件系统类数据读取与保存](#%e6%96%87%e4%bb%b6%e7%b3%bb%e7%bb%9f%e7%b1%bb%e6%95%b0%e6%8d%ae%e8%af%bb%e5%8f%96%e4%b8%8e%e4%bf%9d%e5%ad%98)
      - [HDFS](#hdfs)
      - [MySQL数据库连接](#mysql%e6%95%b0%e6%8d%ae%e5%ba%93%e8%bf%9e%e6%8e%a5)
      - [HBase数据库](#hbase%e6%95%b0%e6%8d%ae%e5%ba%93)
  - [RDD 的持久化](#rdd-%e7%9a%84%e6%8c%81%e4%b9%85%e5%8c%96)
    - [RDD 的 cache(持久化)](#rdd-%e7%9a%84-cache%e6%8c%81%e4%b9%85%e5%8c%96)
    - [持久化需求](#%e6%8c%81%e4%b9%85%e5%8c%96%e9%9c%80%e6%b1%82)

# Spark基础

## spark来历

1. MapReduce有很多弊端

2. yarn使得资源调度（RM）和任务调度（Driver）解耦。通过AM连接。当task在NM中运行，当task需要更多资源的时候，driver通过AM中介，来跟RM交互，拿到资源，最后RM指挥NM来拿到资源给task用

3.spark基于hadoop1.X来做，当时没有yarn，yarn是在hadoop2.X问世的

适合迭代式计算是因为scala是函数式编程，可以不断地调用函数。

中间结果放在内存中。

spark改进hadoop1.0中MapReduce缺点的方案是：

Worker是资源管理，Executor是计算。

Worker可以被yarn的NodeManager代替（资源管理可以被替代）但是计算框架spark比MapReduce要好用，所以当hadoop2.X实现yarn的资源调度+计算可插拔之后，大数据框架组合变成了hadoopHDFS存储+yarn资源调度+spark计算

## Spark内置模块

## Spark特点

## Driver与Excetor

### Driver（驱动器）

Spark的驱动器是执行开发程序中的main方法的进程。它负责开发人员编写的用来创建SparkContext、创建RDD，以及进行RDD的转化操作和行动操作代码的执行。如果你是用spark shell，那么当你启动Spark shell的时候，系统后台自启了一个Spark驱动器程序，就是在Spark shell中预加载的一个叫作 sc的SparkContext对象。如果驱动器程序终止，那么Spark应用也就结束了。主要负责：

1）把用户程序转为作业（JOB）

2）跟踪Executor的运行状况

3）为执行器节点调度任务

4）UI展示应用运行状况

### Executor（执行器）

Spark Executor是一个工作进程，负责在 Spark 作业中运行任务，任务间相互独立。Spark 应用启动时，Executor节点被同时启动，并且始终伴随着整个 Spark 应用的生命周期而存在。如果有Executor节点发生了故障或崩溃，Spark 应用也可以继续执行，会将出错节点上的任务调度到其他Executor节点上继续运行。主要负责：

1）负责运行组成 Spark 应用的任务，并将结果返回给驱动器进程；

2）通过自身的块管理器（Block Manager）为用户程序中要求缓存的RDD提供内存式存储。RDD是直接缓存在Executor进程内的，因此任务可以在运行时充分利用缓存数据加速运算。

## WordCount思路

```scala
package com.atguigu

import org.apache.spark.{SparkConf, SparkContext}

object WordCount{

  def main(args: Array[String]): Unit = {

//1.创建SparkConf并设置App名称
    val conf = new SparkConf().setAppName("WC")

//2.创建SparkContext，该对象是提交Spark App的入口
    val sc = new SparkContext(conf)

    //3.使用sc创建RDD并执行相应的transformation和action
    sc.textFile(args(0)).flatMap(_.split(" ")).map((_, 1)).reduceByKey(_+_, 1).sortBy(_._2, false).saveAsTextFile(args(1))

//4.关闭连接
    sc.stop()
  }
}

```

## Spark在Yarn下运行

## 使用yarn

## 原理

### Yarn的架构

### Spark向Yarn提交任务的过程

提交代码

# RDD

## JavaIO

BufferedInputStream是一个缓冲区

FileInputStream是实际读取文件者

### 字符流的读取

FileInputStream是实际读取文件者

InputStreamReader（new FileInputStream（“xx”），“UTF-8”）；负责把FileInputStream的读取转换成UTF-8的格式

BufferedReader是一个字符缓冲区，把UTF-8的数据放入缓冲区，存满后发给java程序

最后的readline的时候才是真正的java读取

## RDD介绍

RDD(Resilient Distributed Dataset) 叫着 弹性分布式数据集 ，是Spark 中最基本的抽象，它代表一个不可变、可分区、里面元素可以并行计算的集合。

**不可变的意思是内部不提供改变数据的方法，类似于string类型不同改变string的方法，substring之类的函数其实是创建一个新的字符串，不可变是因为rdd的数据要给很多程序来用。**

RDD 具有数据流模型特点：自动容错、位置感知性调度和可伸缩。

RDD 允许用户在执行多个查询时，显示地将工作集缓存在内存中，后续的查询能够重用工作集，这将会极大的提升查询的效率。

我们可以认为 RDD 就是一个代理，我们操作这个代理就像操作本地集合一样，不需去关心任务调度、容错等问题。

### RDD的原理

当触发collect的时候才会执行图中的逻辑。在此之前知识数据处理逻辑的封装。

RDD里面封装的是数据处理逻辑，而非数据。

## RDD 的属性

**一组分片（Partition），即数据集的基本组成单位**。 对于RDD来说，每个分片都会被一个计算任务处理，并决定并行计算的粒度。用户可以在创建RDD 的时候指定RDD的分片个数，如果没有指定，那么就会采用默认值。默认值就是程序所分配到的CPU Cores 的数目；

**对于RDD来说，每个分片都会被一个计算任务处理，并决定并行计算的粒度**。用户可以在创建RDD 的时候指定RDD的分片个数，如果没有指定，那么就会采用默认值。默认值就是程序所分配到的CPU Cores 的数目；

**RDD 之间互相存在依赖关系**。 RDD 的每次转换都会生成一个新的 RDD ,所以 RDD 之前就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时，Spark 可以通过这个依赖关系重新计算丢失部分的分区数据，而不是对 RDD 的所有分区进行重新计算。

**一个Partitioner ，即 RDD 的分片函数** 。当前Spark 中实现了两种类型的分片函数，一个是基于哈希的 HashPartitioner ，另外一个是基于范围的 RangePartitioner。只有对于key-value的RDD ,才会有 Partitioner,非 key-value 的RDD 的 Partitioner 的值是None。Partitioner 函数不但决定了RDD 本身的分片数量，也决定了 Parent RDD Shuffle 输出时的分片数量。

**一个列表，存储存取每个Partition 的优先位置（preferred location）**。 对于一个HDFS 文件来说，这个列表保存的就是每个 Partition 所在的块位置。安装“移动数据不如移动计算”的理念，Spark 在进行任务调度的时候，会尽可能地将计算任务分配到其所要处理数据块的存储位置。

## RDD的特点

RDD表示只读的分区的数据集，对RDD进行改动，只能通过RDD的转换操作，由一个RDD得到一个新的RDD，新的RDD包含了从其他RDD衍生所必需的信息。RDDs之间存在依赖，RDD的执行是按照血缘关系延时计算的。如果血缘关系较长，可以通过持久化RDD来切断血缘关系。

### 分区

RDD逻辑上是分区的，每个分区的数据是抽象存在的，计算的时候会通过一个compute函数得到每个分区的数据。如果RDD是通过已有的文件系统构建，则compute函数是读取指定文件系统中的数据，如果RDD是通过其他RDD转换而来，则compute函数是执行转换逻辑将其他RDD的数据进行转换。

### 只读

RDD是只读的，要想改变RDD中的数据，只能在现有的RDD基础上创建新的RDD。

由一个RDD转换到另一个RDD，可以通过丰富的操作算子实现，不再像MapReduce那样只能写map和reduce了

RDD的操作算子包括两类，一类叫做transformations，它是用来将RDD进行转化，构建RDD的血缘关系；另一类叫做actions，它是用来触发RDD的计算，得到RDD的相关计算结果或者将RDD保存的文件系统中。下图是RDD所支持的操作算子列表。

### 依赖

RDDs通过操作算子进行转换，转换得到的新RDD包含了从其他RDDs衍生所必需的信息，RDDs之间维护着这种血缘关系，也称之为依赖。如下图所示，依赖包括两种，一种是窄依赖，RDDs之间分区是一一对应的，另一种是宽依赖，下游RDD的每个分区与上游RDD(也称之为父RDD)的每个分区都有关，是多对多的关系。

### 缓存

如果在应用程序中多次使用同一个RDD，可以将该RDD缓存起来，该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据，在后续其他地方用到该RDD的时候，会直接从缓存处取而不用再根据血缘关系计算，这样就加速后期的重用。如下图所示，RDD-1经过一系列的转换后得到RDD-n并保存到hdfs，RDD-1在这一过程中会有个中间结果，如果将其缓存到内存，那么在随后的RDD-1转换到RDD-m这一过程中，就不会计算其之前的RDD-0了。

### CheckPoint

虽然RDD的血缘关系天然地可以实现容错，当RDD的某个分区数据失败或丢失，可以通过血缘关系重建。但是对于长时间迭代型应用来说，随着迭代的进行，RDDs之间的血缘关系会越来越长，一旦在后续迭代过程中出错，则需要通过非常长的血缘关系去重建，势必影响性能。为此，RDD支持checkpoint将数据保存到持久化的存储中，这样就可以切断之前的血缘关系，因为checkpoint后的RDD不需要知道它的父RDDs了，它可以从checkpoint处拿到数据。

### 依赖关系

RDD 和它依赖的 父 RDD(可能有多个) 的关系有两种不同的类型，即 窄依赖（narrow dependency）和宽依赖（wide dependency）。

**窄依赖**：窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个分区使用。可以比喻为独生子女。 **宽依赖**：宽依赖是多个字 RDD 的Partition 会依赖同一个父 RDD 的 Partition

## RDD编程

### RDD创建

在Spark中创建RDD的创建方式可以分为三种：从集合中创建RDD；从外部存储创建RDD；从其他RDD创建。

#### 从集合创建

#### 从外部存储系统创建

**分区问题**

### 分区原理

文件分区的分区数是最小分区数，内存分区的分区数是最大分区数。

**最小分区数**

根据读入文件的字节数大小假设是13KB，分区数为2，则分区为

13/2= 6...1

所以分区为

6   6  1

最终有三个分区

**最大分区数**

13被拆分为  6  和 6+1

## RDD的转换

RDD整体上分为Value类型和Key-Value类型

### Value类型

#### map(func)案例

1. 作用：返回一个新的RDD，该RDD由每一个输入元素经过func函数转换后组成

2. 需求：创建一个1-10数组的RDD，将所有元素*2形成新的RDD

####  mapPartitions(func) 案例

作用：类似于map，但独立地在RDD的每一个分片上运行，因此在类型为T的RDD上运行时，func的函数类型必须是Iterator[T] => Iterator[U]。假设有N个元素，有M个分区，那么map的函数的将被调用N次,而mapPartitions被调用M次,一个函数一次处理所有分区。

#### mapPartitionsWithIndex(func) 案例

作用：类似于mapPartitions，但func带有一个整数参数表示分片的索引值，因此在类型为T的RDD上运行时，func的函数类型必须是(Int, Interator[T]) => Iterator[U]；

### Driver与Excetor之间数据的传递

Driver和Executor分别执行不同的代码：

这就会涉及到如果Driver和Executor不在同一台机器，i的传播问题，如果i不能被序列化，则i无法在不同机器间传播，则会报错。

#### latMap(func) 案例

 作用：类似于map，但是每一个输入元素可以被映射为0或多个输出元素（所以func应该返回一个序列，而不是单一元素）

#### map()和mapPartition()的区别

1. map()：每次处理一条数据。

2. mapPartition()：每次处理一个分区的数据，这个分区的数据处理完后，原RDD中分区的数据才能释放，**可能导致OOM。**

3. 开发指导：当内存空间较大的时候建议使用mapPartition()，以提高处理效率。

####  glom案例

作用：将每一个分区形成一个数组，形成新的RDD类型时RDD[Array[T]]

####  groupBy(func)案例

作用：分组，按照传入函数的返回值进行分组。将相同的key对应的值放入一个迭代器。

需求：创建一个RDD，按照元素模以2的值进行分组。

（1）创建

scala> val rdd = sc.parallelize(1 to 4)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[65] at parallelize at <console>:24

（2）按照元素模以2的值进行分组

scala> val group = rdd.groupBy(_%2)

group: org.apache.spark.rdd.RDD[(Int, Iterable[Int])] = ShuffledRDD[2] at groupBy at <console>:26

（3）打印结果

scala> group.collect

res0: Array[(Int, Iterable[Int])] = Array((0,CompactBuffer(2, 4)), (1,CompactBuffer(1, 3)))

#### filter(func) 案例

作用：过滤。返回一个新的RDD，该RDD由经过func函数计算后返回值为true的输入元素组成。

需求：创建一个RDD（由字符串组成），过滤出一个新RDD（包含”xiao”子串）

（1）创建

scala> var sourceFilter = sc.parallelize(Array("xiaoming","xiaojiang","xiaohe","dazhi"))

sourceFilter: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[10] at parallelize at <console>:24

（2）打印

scala> sourceFilter.collect()

res9: Array[String] = Array(xiaoming, xiaojiang, xiaohe, dazhi)

（3）过滤出含” xiao”子串的形成一个新的RDD

scala> val filter = sourceFilter.filter(_.contains("xiao"))

filter: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[11] at filter at <console>:26

（4）打印新RDD

scala> filter.collect()

res10: Array[String] = Array(xiaoming, xiaojiang, xiaohe)

#### sample(withReplacement, fraction, seed) 案例

作用：以指定的随机种子随机抽样出数量为fraction的数据，withReplacement表示是抽出的数据是否放回，true为有放回的抽样，false为无放回的抽样，seed用于指定随机数生成器种子。

需求：创建一个RDD（1-10），从中选择放回和不放回抽样

（1）创建RDD

scala> val rdd = sc.parallelize(1 to 10)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[20] at parallelize at <console>:24

（2）打印

scala> rdd.collect()

res15: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

（3）放回抽样

scala> var sample1 = rdd.sample(true,0.4,2)

sample1: org.apache.spark.rdd.RDD[Int] = PartitionwiseSampledRDD[21] at sample at <console>:26

（4）打印放回抽样结果

scala> sample1.collect()

res16: Array[Int] = Array(1, 2, 2, 7, 7, 8, 9)

（5）不放回抽样

scala> var sample2 = rdd.sample(false,0.2,3)

sample2: org.apache.spark.rdd.RDD[Int] = PartitionwiseSampledRDD[22] at sample at <console>:26

（6）打印不放回抽样结果

scala> sample2.collect()

res17: Array[Int] = Array(1, 9)

#### distinct([numTasks])) 案例

作用：对源RDD进行去重后返回一个新的RDD。默认情况下，只有8个并行任务来操作，但是可以传入一个可选的numTasks参数改变它。

需求：创建一个RDD，使用distinct()对其去重。

（1）创建一个RDD

scala> val distinctRdd = sc.parallelize(List(1,2,1,5,2,9,6,1))

distinctRdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[34] at parallelize at <console>:24

（2）对RDD进行去重（不指定并行度）

scala> val unionRDD = distinctRdd.distinct()

unionRDD: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[37] at distinct at <console>:26

（3）打印去重后生成的新RDD

scala> unionRDD.collect()

res20: Array[Int] = Array(1, 9, 5, 6, 2)

（4）对RDD（指定并行度为2）

scala> val unionRDD = distinctRdd.distinct(2)

unionRDD: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[40] at distinct at <console>:26

（5）打印去重后生成的新RDD

scala> unionRDD.collect()

res21: Array[Int] = Array(6, 2, 1, 9, 5)

#### coalesce(numPartitions) 案例

作用：缩减分区数，用于大数据集过滤后，提高小数据集的执行效率。

需求：创建一个4个分区的RDD，对其缩减分区

（1）创建一个RDD

scala> val rdd = sc.parallelize(1 to 16,4)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[54] at parallelize at <console>:24

（2）查看RDD的分区数

scala> rdd.partitions.size

res20: Int = 4

（3）对RDD重新分区

scala> val coalesceRDD = rdd.coalesce(3)

coalesceRDD: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[55] at coalesce at <console>:26

（4）查看新RDD的分区数

scala> coalesceRDD.partitions.size

res21: Int = 3

#### repartition(numPartitions) 案例

作用：根据分区数，重新通过网络随机洗牌所有数据。

需求：创建一个4个分区的RDD，对其重新分区

（1）创建一个RDD

scala> val rdd = sc.parallelize(1 to 16,4)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[56] at parallelize at <console>:24

（2）查看RDD的分区数

scala> rdd.partitions.size

res22: Int = 4

（3）对RDD重新分区

scala> val rerdd = rdd.repartition(2)

rerdd: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[60] at repartition at <console>:26

（4）查看新RDD的分区数

scala> rerdd.partitions.size

res23: Int = 2

####  coalesce和repartition的区别

coalesce重新分区，可以选择是否进行shuffle过程。由参数shuffle: Boolean = false/true决定。

repartition实际上是调用的coalesce，默认是进行shuffle的。源码如下：

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
  coalesce(numPartitions, shuffle = true)
 }

#### sortBy(func,[ascending], [numTasks]) 案例

作用；使用func先对数据进行处理，按照处理后的数据比较结果排序，默认为正序。

需求：创建一个RDD，按照不同的规则进行排序

（1）创建一个RDD

scala> val rdd = sc.parallelize(List(2,1,3,4))

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[21] at parallelize at <console>:24

（2）按照自身大小排序

scala> rdd.sortBy(x => x).collect()

res11: Array[Int] = Array(1, 2, 3, 4)

（3）按照与3余数的大小排序

scala> rdd.sortBy(x => x%3).collect()

res12: Array[Int] = Array(3, 4, 1, 2)

#### pipe(command, [envVars]) 案例

作用：管道，针对每个分区，都执行一个shell脚本，返回输出的RDD。

注意：脚本需要放在Worker节点可以访问到的位置

需求：编写一个脚本，使用管道将脚本作用于RDD上。

（1）编写一个脚本

Shell脚本

\#!/bin/sh

echo "AA"

while read LINE; do

  echo ">>>"${LINE}

done

（2）创建一个只有一个分区的RDD

scala> val rdd = sc.parallelize(List("hi","Hello","how","are","you"),1)

rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[50] at parallelize at <console>:24

（3）将脚本作用该RDD并打印

scala> rdd.pipe("/opt/module/spark/pipe.sh").collect()

res18: Array[String] = Array(AA, >>>hi, >>>Hello, >>>how, >>>are, >>>you)

（4）创建一个有两个分区的RDD

scala> val rdd = sc.parallelize(List("hi","Hello","how","are","you"),2)

rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[52] at parallelize at <console>:24

（5）将脚本作用该RDD并打印

scala> rdd.pipe("/opt/module/spark/pipe.sh").collect()

res19: Array[String] = Array(AA, >>>hi, >>>Hello, AA, >>>how, >>>are, >>>you)

###  双Value类型交互

#### 2.3.2.1 union(otherDataset) 案例

\1. 作用：对源RDD和参数RDD求并集后返回一个新的RDD

\2. 需求：创建两个RDD，求并集

（1）创建第一个RDD

scala> val rdd1 = sc.parallelize(1 to 5)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[23] at parallelize at <console>:24

（2）创建第二个RDD

scala> val rdd2 = sc.parallelize(5 to 10)

rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[24] at parallelize at <console>:24

（3）计算两个RDD的并集

scala> val rdd3 = rdd1.union(rdd2)

rdd3: org.apache.spark.rdd.RDD[Int] = UnionRDD[25] at union at <console>:28

（4）打印并集结果

scala> rdd3.collect()

res18: Array[Int] = Array(1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10)

#### 2.3.2.2 subtract (otherDataset) 案例

\1. 作用：计算差的一种函数，去除两个RDD中相同的元素，不同的RDD将保留下来

\2. 需求：创建两个RDD，求第一个RDD与第二个RDD的差集

（1）创建第一个RDD

scala> val rdd = sc.parallelize(3 to 8)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[70] at parallelize at <console>:24

（2）创建第二个RDD

scala> val rdd1 = sc.parallelize(1 to 5)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[71] at parallelize at <console>:24

（3）计算第一个RDD与第二个RDD的差集并打印

scala> rdd.subtract(rdd1).collect()

res27: Array[Int] = Array(8, 6, 7)

#### 2.3.2.3 intersection(otherDataset) 案例

\1. 作用：对源RDD和参数RDD求交集后返回一个新的RDD

\2. 需求：创建两个RDD，求两个RDD的交集

（1）创建第一个RDD

scala> val rdd1 = sc.parallelize(1 to 7)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[26] at parallelize at <console>:24

（2）创建第二个RDD

scala> val rdd2 = sc.parallelize(5 to 10)

rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[27] at parallelize at <console>:24

（3）计算两个RDD的交集

scala> val rdd3 = rdd1.intersection(rdd2)

rdd3: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[33] at intersection at <console>:28

（4）打印计算结果

scala> rdd3.collect()

res19: Array[Int] = Array(5, 6, 7)

#### 2.3.2.4 cartesian(otherDataset) 案例

\1. 作用：笛卡尔积（尽量避免使用）

\2. 需求：创建两个RDD，计算两个RDD的笛卡尔积

（1）创建第一个RDD

scala> val rdd1 = sc.parallelize(1 to 3)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[47] at parallelize at <console>:24

（2）创建第二个RDD

scala> val rdd2 = sc.parallelize(2 to 5)

rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[48] at parallelize at <console>:24

（3）计算两个RDD的笛卡尔积并打印

scala> rdd1.cartesian(rdd2).collect()

res17: Array[(Int, Int)] = Array((1,2), (1,3), (1,4), (1,5), (2,2), (2,3), (2,4), (2,5), (3,2), (3,3), (3,4), (3,5))

#### 2.3.2.5 zip(otherDataset)案例

\1. 作用：将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同，否则会抛出异常。

\2. 需求：创建两个RDD，并将两个RDD组合到一起形成一个(k,v)RDD

（1）创建第一个RDD

scala> val rdd1 = sc.parallelize(Array(1,2,3),3)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at parallelize at <console>:24

（2）创建第二个RDD（与1分区数相同）

scala> val rdd2 = sc.parallelize(Array("a","b","c"),3)

rdd2: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[2] at parallelize at <console>:24

（3）第一个RDD组合第二个RDD并打印

scala> rdd1.zip(rdd2).collect

res1: Array[(Int, String)] = Array((1,a), (2,b), (3,c))

（4）第二个RDD组合第一个RDD并打印

scala> rdd2.zip(rdd1).collect

res2: Array[(String, Int)] = Array((a,1), (b,2), (c,3))

（5）创建第三个RDD（与1,2分区数不同）

scala> val rdd3 = sc.parallelize(Array("a","b","c"),2)

rdd3: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[5] at parallelize at <console>:24

（6）第一个RDD组合第三个RDD并打印

scala> rdd1.zip(rdd3).collect

java.lang.IllegalArgumentException: Can't zip RDDs with unequal numbers of partitions: List(3, 2)

 at org.apache.spark.rdd.ZippedPartitionsBaseRDD.getPartitions(ZippedPartitionsRDD.scala:57)

 at org.apache.spark.rdd.RDD$$anonfun$partitions$2.apply(RDD.scala:252)

 at org.apache.spark.rdd.RDD$$anonfun$partitions$2.apply(RDD.scala:250)

 at scala.Option.getOrElse(Option.scala:121)

 at org.apache.spark.rdd.RDD.partitions(RDD.scala:250)

 at org.apache.spark.SparkContext.runJob(SparkContext.scala:1965)

 at org.apache.spark.rdd.RDD$$anonfun$collect$1.apply(RDD.scala:936)

 at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:151)

 at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:112)

 at org.apache.spark.rdd.RDD.withScope(RDD.scala:362)

 at org.apache.spark.rdd.RDD.collect(RDD.scala:935)

 ... 48 elided

### 2.3.3 Key-Value类型

#### 2.3.3.1 partitionBy案例

\1. 作用：对pairRDD进行分区操作，如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区， 否则会生成ShuffleRDD，即会产生shuffle过程。

\2. 需求：创建一个4个分区的RDD，对其重新分区

（1）创建一个RDD

scala> val rdd = sc.parallelize(Array((1,"aaa"),(2,"bbb"),(3,"ccc"),(4,"ddd")),4)

rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[44] at parallelize at <console>:24

（2）查看RDD的分区数

scala> rdd.partitions.size

res24: Int = 4

（3）对RDD重新分区

scala> var rdd2 = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))

rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ShuffledRDD[45] at partitionBy at <console>:26

（4）查看新RDD的分区数

scala> rdd2.partitions.size

res25: Int = 2

#### 2.3.3.2 groupByKey案例

\1. 作用：groupByKey也是对每个key进行操作，但只生成一个sequence。

\2. 需求：创建一个pairRDD，将相同key对应值聚合到一个sequence中，并计算相同key对应值的相加结果。

（1）创建一个pairRDD

scala> val words = Array("one", "two", "two", "three", "three", "three")

words: Array[String] = Array(one, two, two, three, three, three)

scala> val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))

wordPairsRDD: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[4] at map at <console>:26

（2）将相同key对应值聚合到一个sequence中

scala> val group = wordPairsRDD.groupByKey()

group: org.apache.spark.rdd.RDD[(String, Iterable[Int])] = ShuffledRDD[5] at groupByKey at <console>:28

（3）打印结果

scala> group.collect()

res1: Array[(String, Iterable[Int])] = Array((two,CompactBuffer(1, 1)), (one,CompactBuffer(1)), (three,CompactBuffer(1, 1, 1)))

（4）计算相同key对应值的相加结果

scala> group.map(t => (t._1, t._2.sum))

res2: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[6] at map at <console>:31

（5）打印结果

scala> res2.collect()

res3: Array[(String, Int)] = Array((two,2), (one,1), (three,3))

#### 2.3.3.3 reduceByKey(func, [numTasks]) 案例

\1. 在一个(K,V)的RDD上调用，返回一个(K,V)的RDD，使用指定的reduce函数，将相同key的值聚合到一起，reduce任务的个数可以通过第二个可选的参数来设置。

\2. 需求：创建一个pairRDD，计算相同key对应值的相加结果

（1）创建一个pairRDD

scala> val rdd = sc.parallelize(List(("female",1),("male",5),("female",5),("male",2)))

rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[46] at parallelize at <console>:24

（2）计算相同key对应值的相加结果

scala> val reduce = rdd.reduceByKey((x,y) => x+y)

reduce: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[47] at reduceByKey at <console>:26

（3）打印结果

scala> reduce.collect()

res29: Array[(String, Int)] = Array((female,6), (male,7))

#### 2.3.3.4 reduceByKey和groupByKey的区别

\1. reduceByKey：按照key进行聚合，在shuffle之前有combine（预聚合）操作，返回结果是RDD[k,v].

\2. groupByKey：按照key进行分组，直接进行shuffle。

\3. 开发指导：reduceByKey比groupByKey，建议使用。但是需要注意是否会影响业务逻辑。

#### 2.3.3.5 aggregateByKey案例

参数：(zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U) => U)

\1. 作用：在kv对的RDD中，，按key将value进行分组合并，合并时，将每个value和初始值作为seq函数的参数，进行计算，返回的结果作为一个新的kv对，然后再将结果按照key进行合并，最后将每个分组的value传递给combine函数进行计算（先将前两个value进行计算，将返回结果和下一个value传给combine函数，以此类推），将key与计算结果作为一个新的kv对输出。

\2. 参数描述：

（1）zeroValue：给每一个分区中的每一个key一个初始值；

（2）seqOp：函数用于在每一个分区中用初始值逐步迭代value；

（3）combOp：函数用于合并每个分区中的结果。

\3. 需求：创建一个pairRDD，取出每个分区相同key对应值的最大值，然后相加

\4. 需求分析

（1）创建一个pairRDD

scala> val rdd = sc.parallelize(List(("a",3),("a",2),("c",4),("b",3),("c",6),("c",8)),2)

rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at <console>:24

（2）取出每个分区相同key对应值的最大值，然后相加

scala> val agg = rdd.aggregateByKey(0)(math.max(_,_),_+_)

agg: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[1] at aggregateByKey at <console>:26

（3）打印结果

scala> agg.collect()

res0: Array[(String, Int)] = Array((b,3), (a,3), (c,12))

#### 2.3.3.6 foldByKey案例

参数：(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]

\1.   作用：aggregateByKey的简化操作，seqop和combop相同

\2.   需求：创建一个pairRDD，计算相同key对应值的相加结果

（1）创建一个pairRDD

scala> val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)

rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[91] at parallelize at <console>:24

（2）计算相同key对应值的相加结果

scala> val agg = rdd.foldByKey(0)(_+_)

agg: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[92] at foldByKey at <console>:26

（3）打印结果

scala> agg.collect()

res61: Array[(Int, Int)] = Array((3,14), (1,9), (2,3))

#### 2.3.3.7 combineByKey[C] 案例

参数：(createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C)

\1.   作用：对相同K，把V合并成一个集合。

\2.   参数描述：

（1）createCombiner: combineByKey() 会遍历分区中的所有元素，因此每个元素的键要么还没有遇到过，要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初始值

（2）mergeValue: 如果这是一个在处理当前分区之前已经遇到的键，它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并

（3）mergeCombiners: 由于每个分区都是独立处理的， 因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器， 就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。

\3.   需求：创建一个pairRDD，根据key计算每种key的均值。（先计算每个key出现的次数以及可以对应值的总和，再相除得到结果）

\4.   需求分析：

（1）创建一个pairRDD

scala> val input = sc.parallelize(Array(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)),2)

input: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[52] at parallelize at <console>:26

（2）将相同key对应的值相加，同时记录该key出现的次数，放入一个二元组

scala> val combine = input.combineByKey((_,1),(acc:(Int,Int),v)=>(acc._1+v,acc._2+1),(acc1:(Int,Int),acc2:(Int,Int))=>(acc1._1+acc2._1,acc1._2+acc2._2))

combine: org.apache.spark.rdd.RDD[(String, (Int, Int))] = ShuffledRDD[5] at combineByKey at <console>:28

（3）打印合并后的结果

scala> combine.collect

res5: Array[(String, (Int, Int))] = Array((b,(286,3)), (a,(274,3)))

（4）计算平均值

scala> val result = combine.map{case (key,value) => (key,value._1/value._2.toDouble)}

result: org.apache.spark.rdd.RDD[(String, Double)] = MapPartitionsRDD[54] at map at <console>:30

（5）打印结果

scala> result.collect()

res33: Array[(String, Double)] = Array((b,95.33333333333333), (a,91.33333333333333))

#### 2.3.3.8 sortByKey([ascending], [numTasks]) 案例

\1. 作用：在一个(K,V)的RDD上调用，K必须实现Ordered接口，返回一个按照key进行排序的(K,V)的RDD

\2. 需求：创建一个pairRDD，按照key的正序和倒序进行排序

（1）创建一个pairRDD

scala> val rdd = sc.parallelize(Array((3,"aa"),(6,"cc"),(2,"bb"),(1,"dd")))

rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[14] at parallelize at <console>:24

（2）按照key的正序

scala> rdd.sortByKey(true).collect()

res9: Array[(Int, String)] = Array((1,dd), (2,bb), (3,aa), (6,cc))

（3）按照key的倒序

scala> rdd.sortByKey(false).collect()

res10: Array[(Int, String)] = Array((6,cc), (3,aa), (2,bb), (1,dd))

#### 2.3.3.9 mapValues案例

\1. 针对于(K,V)形式的类型只对V进行操作

\2. 需求：创建一个pairRDD，并将value添加字符串"|||"

（1）创建一个pairRDD

scala> val rdd3 = sc.parallelize(Array((1,"a"),(1,"d"),(2,"b"),(3,"c")))

rdd3: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[67] at parallelize at <console>:24

（2）对value添加字符串"|||"

scala> rdd3.mapValues(_+"|||").collect()

res26: Array[(Int, String)] = Array((1,a|||), (1,d|||), (2,b|||), (3,c|||))

#### 2.3.3.10 join(otherDataset, [numTasks]) 案例

\1. 作用：在类型为(K,V)和(K,W)的RDD上调用，返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD

\2. 需求：创建两个pairRDD，并将key相同的数据聚合到一个元组。

（1）创建第一个pairRDD

scala> val rdd = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c")))

rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[32] at parallelize at <console>:24

（2）创建第二个pairRDD

scala> val rdd1 = sc.parallelize(Array((1,4),(2,5),(3,6)))

rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[33] at parallelize at <console>:24

（3）join操作并打印结果

scala> rdd.join(rdd1).collect()

res13: Array[(Int, (String, Int))] = Array((1,(a,4)), (2,(b,5)), (3,(c,6)))

#### 2.3.3.11 cogroup(otherDataset, [numTasks]) 案例

\1. 作用：在类型为(K,V)和(K,W)的RDD上调用，返回一个(K,(Iterable<V>,Iterable<W>))类型的RDD

\2. 需求：创建两个pairRDD，并将key相同的数据聚合到一个迭代器。

（1）创建第一个pairRDD

scala> val rdd = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c")))

rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[37] at parallelize at <console>:24

（2）创建第二个pairRDD

scala> val rdd1 = sc.parallelize(Array((1,4),(2,5),(3,6)))

rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[38] at parallelize at <console>:24

（3）cogroup两个RDD并打印结果

scala> rdd.cogroup(rdd1).collect()

res14: Array[(Int, (Iterable[String], Iterable[Int]))] = Array((1,(CompactBuffer(a),CompactBuffer(4))), (2,(CompactBuffer(b),CompactBuffer(5))), (3,(CompactBuffer(c),CompactBuffer(6))))

### 2.3.4 案例实操

\1. 数据结构：时间戳，省份，城市，用户，广告，中间字段使用空格分割。

样本如下：

1516609143867 6 7 64 16

1516609143869 9 4 75 18

1516609143869 1 7 87 12

\2. 需求：统计出每一个省份广告被点击次数的TOP3

\3. 实现过程：

package com.package.practice

import org.apache.spark.rdd.RDD

import org.apache.spark.{SparkConf, SparkContext}

//需求：统计出每一个省份广告被点击次数的TOP3

object Practice {

 def main(args: Array[String]): Unit = {

  //1.初始化spark配置信息并建立与spark的连接

  val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Practice")

  val sc = new SparkContext(sparkConf)

  //2.读取数据生成RDD：TS，Province，City，User，AD

  val line = sc.textFile("E:\\IDEAWorkSpace\\SparkTest\\src\\main\\resources\\agent.log")

  //3.按照最小粒度聚合：((Province,AD),1)

  val provinceAdToOne = line.map { x =>

   val fields: Array[String] = x.split(" ")

   ((fields(1), fields(4)), 1)

  }

  //4.计算每个省中每个广告被点击的总数：((Province,AD),sum)

  val provinceAdToSum = provinceAdToOne.reduceByKey(_ + _)

  //5.将省份作为key，广告加点击数为value：(Province,(AD,sum))

  val provinceToAdSum = provinceAdToSum.map(x => (x._1._1, (x._1._2, x._2)))

  //6.将同一个省份的所有广告进行聚合(Province,List((AD1,sum1),(AD2,sum2)...))

  val provinceGroup = provinceToAdSum.groupByKey()

  //7.对同一个省份所有广告的集合进行排序并取前3条，排序规则为广告点击总数

  val provinceAdTop3 = provinceGroup.mapValues { x =>

   x.toList.sortWith((x, y) => x._2 > y._2).take(3)

  }

  //8.将数据拉取到Driver端并打印

  provinceAdTop3.collect().foreach(println)

  //9.关闭与spark的连接

  sc.stop()

 }

}

## 2.4 Action

### 2.4.1 reduce(func)案例

\1. 作用：通过func函数聚集RDD中的所有元素，先聚合分区内数据，再聚合分区间数据。

\2. 需求：创建一个RDD，将所有元素聚合得到结果。

（1）创建一个RDD[Int]

scala> val rdd1 = sc.makeRDD(1 to 10,2)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[85] at makeRDD at <console>:24

（2）聚合RDD[Int]所有元素

scala> rdd1.reduce(_+_)

res50: Int = 55

（3）创建一个RDD[String]

scala> val rdd2 = sc.makeRDD(Array(("a",1),("a",3),("c",3),("d",5)))

rdd2: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[86] at makeRDD at <console>:24

（4）聚合RDD[String]所有数据

**key与key拼凑，value与value拼凑**

scala> rdd2.reduce((x,y)=>(x._1 + y._1,x._2 + y._2))

res51: (String, Int) = (adca,12)

### 2.4.2 collect()案例

\1. 作用：在驱动程序中，以数组的形式返回数据集的所有元素。

\2. 需求：创建一个RDD，并将RDD内容收集到Driver端打印

（1）创建一个RDD

scala> val rdd = sc.parallelize(1 to 10)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

（2）将结果收集到Driver端

scala> rdd.collect

res0: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

### 2.4.3 count()案例

\1. 作用：返回RDD中元素的个数

\2. 需求：创建一个RDD，统计该RDD的条数

（1）创建一个RDD

scala> val rdd = sc.parallelize(1 to 10)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

（2）统计该RDD的条数

scala> rdd.count

res1: Long = 10

### 2.4.4 first()案例

\1. 作用：返回RDD中的第一个元素

\2. 需求：创建一个RDD，返回该RDD中的第一个元素

（1）创建一个RDD

scala> val rdd = sc.parallelize(1 to 10)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

（2）统计该RDD的条数

scala> rdd.first

res2: Int = 1

### 2.4.5 take(n)案例

\1. 作用：返回一个由RDD的前n个元素组成的数组

\2. 需求：创建一个RDD，统计该RDD的条数

（1）创建一个RDD

scala> val rdd = sc.parallelize(Array(2,5,4,6,8,3))

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at <console>:24

（2）统计该RDD的条数

scala> rdd.take(3)

res10: Array[Int] = Array(2, 5, 4)

### 2.4.6 takeOrdered(n)案例

\1. 作用：返回该RDD排序后的前n个元素组成的数组

\2. 需求：创建一个RDD，统计该RDD的条数

（1）创建一个RDD

scala> val rdd = sc.parallelize(Array(2,5,4,6,8,3))

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at <console>:24

（2）统计该RDD的条数

scala> rdd.takeOrdered(3)

res18: Array[Int] = Array(2, 3, 4)

### 2.4.7 aggregate案例

\1. 参数：(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)

\2. 作用：aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合，然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。

\3. 需求：创建一个RDD，将所有元素相加得到结果

（1）创建一个RDD

scala> var rdd1 = sc.makeRDD(1 to 10,2)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[88] at makeRDD at <console>:24

（2）将该RDD所有元素相加得到结果

scala> rdd.aggregate(0)(_+_,_+_)

res22: Int = 55

### 2.4.8 fold(num)(func)案例

\1. 作用：折叠操作，aggregate的简化操作，seqop和combop一样。

\2. 需求：创建一个RDD，将所有元素相加得到结果

（1）创建一个RDD

scala> var rdd1 = sc.makeRDD(1 to 10,2)

rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[88] at makeRDD at <console>:24

（2）将该RDD所有元素相加得到结果

scala> rdd.fold(0)(_+_)

res24: Int = 55

### 2.4.9 saveAsTextFile(path)

作用：将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统，对于每个元素，Spark将会调用toString方法，将它装换为文件中的文本

### 2.4.10 saveAsSequenceFile(path)

作用：将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下，可以使HDFS或者其他Hadoop支持的文件系统。

### 2.4.11 saveAsObjectFile(path)

作用：用于将RDD中的元素序列化成对象，存储到文件中。

### 2.4.12 countByKey()案例

\1. 作用：针对(K,V)类型的RDD，返回一个(K,Int)的map，表示每一个key对应的元素个数。

\2. 需求：创建一个PairRDD，统计每种key的个数

（1）创建一个PairRDD

scala> val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)

rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[95] at parallelize at <console>:24

（2）统计每种key的个数

scala> rdd.countByKey

res63: scala.collection.Map[Int,Long] = Map(3 -> 2, 1 -> 3, 2 -> 1)

### 2.4.13 foreach(func)案例

\1. 作用：在数据集的每一个元素上，运行函数func进行更新。

\2. 需求：创建一个RDD，对每个元素进行打印

（1）创建一个RDD

scala> var rdd = sc.makeRDD(1 to 5,2)

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[107] at makeRDD at <console>:24

（2）对该RDD每个元素进行打印

scala> rdd.foreach(println(_))

3

4

5

1

2

## RDD中的函数传递

在实际开发中我们往往需要自己定义一些对于RDD的操作，那么此时需要主要的是，初始化工作是在Driver端进行的，而实际运行程序是在Executor端进行的，这就涉及到了跨进程通信，是需要序列化的。下面我们看几个例子：

### 传递一个方法

1．创建一个类

```scala
class Search(s:String){

 //过滤出包含字符串的数据

 def isMatch(s: String): Boolean = {

  s.contains(query)

 }

//过滤出包含字符串的RDD

 def getMatch1 (rdd: RDD[String]): RDD[String] = {

  rdd.filter(isMatch)

 }

 //过滤出包含字符串的RDD

 def getMatche2(rdd: RDD[String]): RDD[String] = {
  rdd.filter(x => x.contains(query))
 }
}
```

2．创建Spark主程序

```scala
object SeriTest {

 def main(args: Array[String]): Unit = {

  //1.初始化配置信息及SparkContext

  val sparkConf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")

  val sc = new SparkContext(sparkConf)

//2.创建一个RDD

  val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))

//3.创建一个Search对象

  val search = new Search()

//4.运用第一个过滤函数并打印结果

  val match1: RDD[String] = search.getMatche1(rdd)

  match1.collect().foreach(println)

  }

}
```

3．运行程序

```

Exception in thread "main" org.apache.spark.SparkException: Task not serializable

  at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)

  at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)

  at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)

  at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)

  at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:387)

  at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:386)

  at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:151)

  at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:112)

  at org.apache.spark.rdd.RDD.withScope(RDD.scala:362)

  at org.apache.spark.rdd.RDD.filter(RDD.scala:386)

  at com.atguigu.Search.getMatche1(SeriTest.scala:39)

  at com.atguigu.SeriTest$.main(SeriTest.scala:18)

  at com.atguigu.SeriTest.main(SeriTest.scala)

Caused by: java.io.NotSerializableException: com.atguigu.Search
```

4．问题说明

//过滤出包含字符串的RDD

 def getMatch1 (rdd: RDD[String]): RDD[String] = {

  rdd.filter(isMatch)

 }

**在这个方法中所调用的方法isMatch()是定义在Search这个类中的，实际上调用的是this. isMatch()，this表示Search这个类的对象，程序在运行过程中需要将Search对象序列化以后传递到Executor端。**

5．解决方案

使类继承scala.Serializable即可。

class Search() extends Serializable{...}

### 2.5.2 传递一个属性

1．创建Spark主程序

```scala
object TransmitTest {

 def main(args: Array[String]): Unit = {

  //1.初始化配置信息及SparkContext

  val sparkConf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")

  val sc = new SparkContext(sparkConf)

//2.创建一个RDD

  val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))

//3.创建一个Search对象

  val search = new Search()

//4.运用第一个过滤函数并打印结果

  val match1: RDD[String] = search.getMatche2(rdd)

  match1.collect().foreach(println)

  }
}
```

2．运行程序

```
Exception in thread "main" org.apache.spark.SparkException: Task not serializable

  at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)

  at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)

  at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)

  at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)

  at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:387)

  at org.apache.spark.rdd.RDD$$anonfun$filter$1.apply(RDD.scala:386)

  at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:151)

  at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:112)

  at org.apache.spark.rdd.RDD.withScope(RDD.scala:362)

  at org.apache.spark.rdd.RDD.filter(RDD.scala:386)

  at com.atguigu.Search.getMatche1(SeriTest.scala:39)

  at com.atguigu.SeriTest$.main(SeriTest.scala:18)

  at com.atguigu.SeriTest.main(SeriTest.scala)

Caused by: java.io.NotSerializableException: com.atguigu.Search
```

3．问题说明

 //过滤出包含字符串的RDD

 def getMatche2(rdd: RDD[String]): RDD[String] = {

  rdd.filter(x => x.contains(query))

 }

**在这个方法中所调用的方法query是定义在Search这个类中的字段，实际上调用的是this. query，this表示Search这个类的对象，程序在运行过程中需要将Search对象序列化以后传递到Executor端。**

4．解决方案

1）使类继承scala.Serializable即可。

class Search() extends Serializable{...}

2）将类变量query赋值给局部变量

修改getMatche2为

 //过滤出包含字符串的RDD

 def getMatche2(rdd: RDD[String]): RDD[String] = {

  val query_ : String = this.query//将类变量赋值给局部变量

  rdd.filter(x => x.contains(query_))

 }

**第二种方法，`val query_ : String = this.query//将类变量赋值给局部变量`在Driver中运行，然后把`query_`传入到Executor里面，`query_`是字符串,字符串自带序列化.**

## RDD依赖关系

###  Lineage

RDD只支持粗粒度转换，即在大量记录上执行的单个操作。将创建RDD的一系列Lineage（血统）记录下来，以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为，当该RDD的部分分区数据丢失时，它可以根据这些信息来重新运算和恢复丢失的数据分区。

（1）读取一个HDFS文件并将其中内容映射成一个个元组

scala> val wordAndOne = sc.textFile("/fruit.tsv").flatMap(_.split("\t")).map((_,1))

wordAndOne: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[22] at map at <console>:24

（2）统计每一种key对应的个数

scala> val wordAndCount = wordAndOne.reduceByKey(_+_)

wordAndCount: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[23] at reduceByKey at <console>:26

（3）查看“wordAndOne”的Lineage

scala> wordAndOne.toDebugString

res5: String =

(2) MapPartitionsRDD[22] at map at <console>:24 []

 | MapPartitionsRDD[21] at flatMap at <console>:24 []

 | /fruit.tsv MapPartitionsRDD[20] at textFile at <console>:24 []

 | /fruit.tsv HadoopRDD[19] at textFile at <console>:24 []

（4）查看“wordAndCount”的Lineage

scala> wordAndCount.toDebugString

res6: String =

(2) ShuffledRDD[23] at reduceByKey at <console>:26 []

 +-(2) MapPartitionsRDD[22] at map at <console>:24 []

  | MapPartitionsRDD[21] at flatMap at <console>:24 []

  | /fruit.tsv MapPartitionsRDD[20] at textFile at <console>:24 []

  | /fruit.tsv HadoopRDD[19] at textFile at <console>:24 []

（5）查看“wordAndOne”的依赖类型

scala> wordAndOne.dependencies

res7: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@5d5db92b)

（6）查看“wordAndCount”的依赖类型

scala> wordAndCount.dependencies

res8: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@63f3e6a8)

注意：RDD和它依赖的父RDD（s）的关系有两种不同的类型，即窄依赖（narrow dependency）和宽依赖（wide dependency）。

### 窄依赖

窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用,窄依赖我们形象的比喻为独生子女

### 宽依赖

宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition，会引起shuffle,总结：宽依赖我们形象的比喻为超生

###  DAG

DAG(Directed Acyclic Graph)叫做有向无环图，原始的RDD通过一系列的转换就就形成了DAG，根据RDD之间的依赖关系的不同将DAG划分成不同的Stage，对于窄依赖，partition的转换处理在Stage中完成计算。对于宽依赖，由于有Shuffle的存在，只能在parent RDD处理完成后，才能开始接下来的计算，因此**宽依赖是划分****Stage****的依据**。

### 任务划分（面试重点）

RDD任务切分中间分为：Application、Job、Stage和Task

1）Application：初始化一个SparkContext即生成一个Application

2）Job：一个**Action算子**就会生成一个Job

3）Stage：根据RDD之间的依赖关系的不同将Job划分成不同的Stage，遇到一个宽依赖则划分一个Stage。

4）Task：Stage是一个TaskSet，将Stage划分的结果发送到不同的Executor执行即为一个Task。

注意：Application->Job->Stage-> Task每一层都是1对n的关系。

**分析**

**比如该图，初始化了一个sc，则有一个application。**

**每有一个action算子，就有一个job，上图有一个action算子，故有一个job**

**一个job中，根据宽依赖和窄依赖（或者说shuffle出现的次数+1）划分成多个stage**

**每个stage中，有多个partition，比如stage2，由于flatmap到map没有shuffle，所以该分区的flatmap和map是串行的，放在一个task里面就可以。**

##  RDD缓存

RDD通过persist方法或cache方法可以将前面的计算结果缓存，默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。

但是并不是这两个方法被调用时立即缓存，而是触发后面的action时，该RDD将会被缓存在计算节点的内存中，并供后面重用。

通过查看源码发现cache最终也是调用了persist方法，默认的存储级别都是仅在内存存储一份，Spark的存储级别还有好多种，存储级别在object StorageLevel中定义的。

缓存有可能丢失，或者存储存储于内存的数据由于内存不足而被删除，RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换，丢失的数据会被重算，由于RDD的各个Partition是相对独立的，因此只需要计算丢失的部分即可，并不需要重算全部Partition。

**关于OFF_HEAP，指的是堆外内存（jvm外面的内存，属于用户向操作系统申请），因为堆内内存的gc是由垃圾收集器自动控制的，不能切换手动，如果在堆内存储数据，无法跟随用户心意马上gc。所以为了实现用户可以对存储数据有强控制，所以在堆外申请一块内存，当用户想清除该内存的数据时，操作系统便会立马清除数据。**

###  RDD CheckPoint

Spark中对于数据的保存除了持久化操作之外，还提供了一种检查点的机制，检查点（本质是通过将RDD写入Disk做检查点）是为了通过lineage做容错的辅助，lineage过长会造成容错成本过高，这样就不如在中间阶段做检查点容错，如果之后有节点出现问题而丢失分区，从做检查点的RDD开始重做Lineage，就会减少开销。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。

为当前RDD设置检查点。该函数将会创建一个二进制的文件，并存储到checkpoint目录中，该目录是用[Spark](https://www.iteblog.com/archives/tag/spark/)Context.setCheckpointDir()设置的。

**在checkpoint的过程中，该RDD的所有依赖于父RDD中的信息将全部被移除。对RDD进行checkpoint操作并不会马上被执行，必须执行Action操作才能触发。**

**但是缓存不会让依赖移除，因为缓存不可靠，会失效。但checkpoint存在hdfs上，并且hdfs有高可用，是可靠的**

案例实操：

（1）设置检查点

```
scala> sc.setCheckpointDir("hdfs://hadoop102:9000/checkpoint")
```

（2）创建一个RDD

```
scala> val rdd = sc.parallelize(Array("atguigu"))

rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[14] at parallelize at <console>:24
```

（3）将RDD转换为携带当前时间戳并做checkpoint

```
scala> val ch = rdd.map(_+System.currentTimeMillis)

ch: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[16] at map at <console>:26
```

scala> ch.checkpoint

（4）多次打印结果

```
scala> ch.collect

res55: Array[String] = Array(atguigu1538981860336)

scala> ch.collect

res56: Array[String] = Array(atguigu1538981860504)

scala> ch.collect

res57: Array[String] = Array(atguigu1538981860504)

scala> ch.collect

res58: Array[String] = Array(atguigu1538981860504)
```

## 键值对RDD数据分区器

Spark目前支持Hash分区和Range分区，用户也可以自定义分区，Hash分区为当前的默认分区，Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数

注意：

**(1)只有Key-Value类型的RDD才有分区器的，非Key-Value类型的RDD分区器的值是None
 (2)每个RDD的分区ID范围：0~numPartitions-1，决定这个值是属于那个分区的。**

### Hash分区

HashPartitioner分区的原理：对于给定的key，计算其hashCode，并除以分区的个数取余，如果余数小于0，则用余数+分区的个数（否则加0），最后返回的值就是这个key所属的分区ID。

使用Hash分区的实操

```
scala> nopar.partitioner

res20: Option[org.apache.spark.Partitioner] = None
scala> val nopar = sc.parallelize(List((1,3),(1,2),(2,4),(2,3),(3,6),(3,8)),8)

nopar: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[10] at parallelize at <console>:24

scala>nopar.mapPartitionsWithIndex((index,iter)=>{ Iterator(index.toString+" : "+iter.mkString("|")) }).collect

res0: Array[String] = Array("0 : ", 1 : (1,3), 2 : (1,2), 3 : (2,4), "4 : ", 5 : (2,3), 6 : (3,6), 7 : (3,8))

scala> val hashpar = nopar.partitionBy(new org.apache.spark.HashPartitioner(7))

hashpar: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[12] at partitionBy at <console>:26

scala> hashpar.count

res18: Long = 6

scala> hashpar.partitioner

res21: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@7)

scala> hashpar.mapPartitions(iter => Iterator(iter.length)).collect()

res19: Array[Int] = Array(0, 3, 1, 2, 0, 0, 0)
```

### Ranger分区

HashPartitioner分区弊端：可能导致每个分区中数据量的不均匀，极端情况下会导致某些分区拥有RDD的全部数据。

RangePartitioner作用：将一定范围内的数映射到某一个分区内，尽量保证每个分区中数据量的均匀，而且分区与分区之间是有序的，一个分区中的元素肯定都是比另一个分区内的元素小或者大，但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。实现过程为：

第一步：先重整个RDD中抽取出样本数据，将样本数据排序，计算出每个分区的最大key值，形成一个Array[KEY]类型的数组变量rangeBounds；

第二步：判断key在rangeBounds中所处的范围，给出该key值在下一个RDD中的分区id下标；该分区器要求RDD中的KEY类型必须是可以排序的

### 自定义分区

要实现自定义的分区器，你需要继承 org.apache.spark.Partitioner 类并实现下面三个方法。

（1）numPartitions: Int:返回创建出来的分区数。

（2）getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)。

（3）equals():Java 判断相等性的标准方法。这个方法的实现非常重要，Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同，这样 Spark 才可以判断两个 RDD 的分区方式是否相同。

需求：将相同后缀的数据写入相同的文件，通过将相同后缀的数据分区到相同的分区并保存输出来实现。

## 数据读取与保存

   Spark的数据读取及数据保存可以从两个维度来作区分：文件格式以及文件系统。

文件格式分为：Text文件、Json文件、Csv文件、Sequence文件以及Object文件；

文件系统分为：本地文件系统、HDFS、HBASE以及数据库

### 文件类数据读取与保存

#### Text文件

```
1）数据读取:textFile(String)

scala> val hdfsFile = sc.textFile("hdfs://hadoop102:9000/fruit.txt")

hdfsFile: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/fruit.txt MapPartitionsRDD[21] at textFile at <console>:24

2）数据保存: saveAsTextFile(String)

scala> hdfsFile.saveAsTextFile("/fruitOut")
```

#### Json文件

如果JSON文件中每一行就是一个JSON记录，那么可以通过将JSON文件当做文本文件来读取，然后利用相关的JSON库对每一条数据进行JSON解析。

注意：使用RDD读取JSON文件处理很复杂，同时SparkSQL集成了很好的处理JSON文件的方式，所以应用中多是采用SparkSQL处理JSON文件。

```
（1）导入解析json所需的包

scala> import scala.util.parsing.json.JSON

（2）上传json文件到HDFS

[atguigu@hadoop102 spark]$ hadoop fs -put ./examples/src/main/resources/people.json /

（3）读取文件

scala> val json = sc.textFile("/people.json")

json: org.apache.spark.rdd.RDD[String] = /people.json MapPartitionsRDD[8] at textFile at <console>:24

（4）解析json数据

scala> val result = json.map(JSON.parseFull)

result: org.apache.spark.rdd.RDD[Option[Any]] = MapPartitionsRDD[10] at map at <console>:27

（5）打印

scala> result.collect

res11: Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age -> 19.0)))
```

#### 对象文件

对象文件是将对象序列化后保存的文件，采用Java的序列化机制。可以通过objectFile[k,v](path) 函数接收一个路径，读取对象文件，返回对应的 RDD，也可以通过调用saveAsObjectFile() 实现对对象文件的输出。因为是序列化所以要指定类型。

```
（1）创建一个RDD

scala> val rdd = sc.parallelize(Array(1,2,3,4))

rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at parallelize at <console>:24

（2）将RDD保存为Object文件

scala> rdd.saveAsObjectFile("file:///opt/module/spark/objectFile")

（3）查看该文件

[atguigu@hadoop102 objectFile]$ pwd

/opt/module/spark/objectFile

[atguigu@hadoop102 objectFile]$ ll

总用量 8

-rw-r--r-- 1 atguigu atguigu 142 10月 9 10:37 part-00000

-rw-r--r-- 1 atguigu atguigu 142 10月 9 10:37 part-00001

-rw-r--r-- 1 atguigu atguigu  0 10月 9 10:37 _SUCCESS

[atguigu@hadoop102 objectFile]$ cat part-00000

SEQ!org.apache.hadoop.io.NullWritable"org.apache.hadoop.io.BytesWritableW@`l

（4）读取Object文件

scala> val objFile = sc.objectFile[Int]("file:///opt/module/spark/objectFile")

objFile: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[31] at objectFile at <console>:24

（5）打印读取后的Sequence文件

scala> objFile.collect

res19: Array[Int] = Array(1, 2, 3, 4)
```

### 文件系统类数据读取与保存

####  HDFS

Spark的整个生态系统与Hadoop是完全兼容的,所以对于Hadoop所支持的文件类型或者数据库类型,Spark也同样支持.另外,由于Hadoop的API有新旧两个版本,所以Spark为了能够兼容Hadoop所有的版本,也提供了两套创建操作接口.对于外部存储创建操作而言,hadoopRDD和newHadoopRDD是最为抽象的两个函数接口,主要包含以下四个参数.

1）输入格式(InputFormat): 制定数据输入的类型,如TextInputFormat等,新旧两个版本所引用的版本分别是org.apache.hadoop.mapred.InputFormat和org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)

2）键类型: 指定[K,V]键值对中K的类型

3）值类型: 指定[K,V]键值对中V的类型

4）分区值: 指定由外部存储生成的RDD的partition数量的最小值,如果没有指定,系统会使用默认值defaultMinSplits

**注意:**其他创建操作的API接口都是为了方便最终的Spark程序开发者而设置的,是这两个接口的高效实现版本.例如,对于textFile而言,只有path这个指定文件路径的参数,其他参数在系统内部指定了默认值。

1.在Hadoop中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为Hadoop本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压.

2.如果用Spark从Hadoop中读取某种类型的数据不知道怎么读取的时候,上网查找一个使用map-reduce的时候是怎么读取这种这种数据的,然后再将对应的读取方式改写成上面的hadoopRDD和newAPIHadoopRDD两个类就行了

#### MySQL数据库连接

支持通过Java JDBC访问关系型数据库。需要通过JdbcRDD进行，示例如下:

```scala
（1）添加依赖
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.27</version>
 </dependency>

（2）Mysql读取：
package com.atguigu

import java.sql.DriverManager

import org.apache.spark.rdd.JdbcRDD

import org.apache.spark.{SparkConf, SparkContext}

object MysqlRDD {

 def main(args: Array[String]): Unit = {
  //1.创建spark配置信息
  val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")

  //2.创建SparkContext

  val sc = new SparkContext(sparkConf)

  //3.定义连接mysql的参数

  val driver = "com.mysql.jdbc.Driver"

  val url = "jdbc:mysql://hadoop102:3306/rdd"

  val userName = "root"

  val passWd = "<PASSWORD>"

  //创建JdbcRDD

  val rdd = new JdbcRDD(sc, () => {

   Class.forName(driver)

   DriverManager.getConnection(url, userName, passWd)

  },

   "select * from `rddtable` where `id`>=?;",
   1,
   10,
   1,
   r => (r.getInt(1), r.getString(2))

  )

  //打印最后结果
  println(rdd.count())
  rdd.foreach(println
  sc.stop()

 }
}
Mysql写入：

def main(args: Array[String]) {
  val sparkConf = new SparkConf().setMaster("local[2]").setAppName("HBaseApp")
  val sc = new SparkContext(sparkConf)
  val data = sc.parallelize(List("Female", "Male","Female"))

  data.foreachPartition(insertData)
 }

 def insertData(iterator: Iterator[String]): Unit = {

Class.forName ("com.mysql.jdbc.Driver").newInstance()
  val conn = java.sql.DriverManager.getConnection("jdbc:mysql://hadoop102:3306/rdd", "root", "000000")
  iterator.foreach(data => {
   val ps = conn.prepareStatement("insert into rddtable(name) values (?)")
   ps.setString(1, data)
   ps.executeUpdate()
  })
 }
```

#### HBase数据库

由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现，Spark 可以通过Hadoop输入格式访问HBase。这个输入格式会返回键值对数据，其中键的类型为org. apache.hadoop.hbase.io.ImmutableBytesWritable，而值的类型为org.apache.hadoop.hbase.client.

Result。

```scala
（1）添加依赖
<dependency>
	<groupId>org.apache.hbase</groupId>
	<artifactId>hbase-server</artifactId>
	<version>1.3.1</version>
</dependency>

<dependency>
	<groupId>org.apache.hbase</groupId>
	<artifactId>hbase-client</artifactId>
	<version>1.3.1</version>
</dependency>
（2）从HBase读取数据
package com.atguigu

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.hadoop.hbase.util.Bytes

object HBaseSpark {

  def main(args: Array[String]): Unit = {

    //创建spark配置信息
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")

    //创建SparkContext
    val sc = new SparkContext(sparkConf)

    //构建HBase配置信息
    val conf: Configuration = HBaseConfiguration.create()
    conf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")
    conf.set(TableInputFormat.INPUT_TABLE, "rddtable")

    //从HBase读取数据形成RDD
    val hbaseRDD: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
      conf,
      classOf[TableInputFormat],
      classOf[ImmutableBytesWritable],
      classOf[Result])

    val count: Long = hbaseRDD.count()
    println(count)

    //对hbaseRDD进行处理
    hbaseRDD.foreach {
      case (_, result) =>
        val key: String = Bytes.toString(result.getRow)
        val name: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name")))
        val color: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("color")))
        println("RowKey:" + key + ",Name:" + name + ",Color:" + color)
    }

    //关闭连接
    sc.stop()
  }

}
3）往HBase写入

def main(args: Array[String]) {
//获取Spark配置信息并创建与spark的连接
  val sparkConf = new SparkConf().setMaster("local[*]").setAppName("HBaseApp")
  val sc = new SparkContext(sparkConf)

//创建HBaseConf
  val conf = HBaseConfiguration.create()
  val jobConf = new JobConf(conf)
  jobConf.setOutputFormat(classOf[TableOutputFormat])
  jobConf.set(TableOutputFormat.OUTPUT_TABLE, "fruit_spark")

//构建Hbase表描述器
  val fruitTable = TableName.valueOf("fruit_spark")
  val tableDescr = new HTableDescriptor(fruitTable)
  tableDescr.addFamily(new HColumnDescriptor("info".getBytes))

//创建Hbase表
  val admin = new HBaseAdmin(conf)
  if (admin.tableExists(fruitTable)) {
    admin.disableTable(fruitTable)
    admin.deleteTable(fruitTable)
  }
  admin.createTable(tableDescr)

//定义往Hbase插入数据的方法
  def convert(triple: (Int, String, Int)) = {
    val put = new Put(Bytes.toBytes(triple._1))
    put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(triple._2))
    put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("price"), Bytes.toBytes(triple._3))
    (new ImmutableBytesWritable, put)
  }

//创建一个RDD
  val initialRDD = sc.parallelize(List((1,"apple",11), (2,"banana",12), (3,"pear",13)))

//将RDD内容写到HBase
  val localData = initialRDD.map(convert)

  localData.saveAsHadoopDataset(jobConf)
}

```

## RDD 的持久化

### RDD 的 cache(持久化)

Spark中最重要的功能之一是跨操作在内存中持久化（或缓存）数据集。当您持久保存RDD时，每个节点都会存储它在内存中计算的任何分区，并在该数据集（或从中派生的数据集）的其他操作中重用它们。这使得未来的行动更快（通常超过10倍）。缓存是迭代算法和快速交互使用的关键工具。

您可以使用persist()或cache()方法标记要保留的RDD 。第一次在动作中计算它，它将保留在节点的内存中。Spark的缓存是容错的 - 如果丢失了RDD的任何分区，它将使用最初创建它的转换自动重新计算。

### 持久化需求

1. 要求的计算速度快
2. 集群的资源要足够大
3. 重要: cache 的数据会多次触发Action
4. 建议先进行数据过滤,然后将缩小范围后的数据再cache 到内存中.

<file_sep>/notes/Hbase.md

- [Hbase基础](#hbase基础)
    - [分布式知识](#分布式知识)
        - [负载均衡](#负载均衡)
            - [负载均衡-客户端决定访问机器](#负载均衡-客户端决定访问机器)
            - [负载均衡-去中心化](#负载均衡-去中心化)
    - [Hbase的特点](#hbase的特点)
    - [Hbase存储设计与mysql对比](#hbase存储设计与mysql对比)
        - [mysql的水平垂直切分  与 列的动态扩展](#mysql的水平垂直切分--与-列的动态扩展)
            - [一条查询语句,mysql是从左向右查,oracle是从右向左查.](#一条查询语句mysql是从左向右查oracle是从右向左查)
            - [宽表与高表](#宽表与高表)
            - [列的动态扩展](#列的动态扩展)
            - [mysql的弊端](#mysql的弊端)
        - [Hbase的解决方法](#hbase的解决方法)
            - [列拆分:](#列拆分)
            - [行拆分:](#行拆分)
            - [加缓存:](#加缓存)
            - [零拷贝:](#零拷贝)
            - [Kafka的吞吐量高的原因:顺序写入+零拷贝+分段日志](#kafka的吞吐量高的原因顺序写入零拷贝分段日志)
- [Hbase存储结构](#hbase存储结构)
    - [逻辑结构](#逻辑结构)
        - [Name Space](#name-space)
        - [Row](#row)
        - [Row Key](#row-key)
        - [列族](#列族)
            - [Region](#region)
            - [Store](#store)
    - [物理存储(store的实际存储方式)](#物理存储store的实际存储方式)
        - [Type](#type)
        - [时间戳](#时间戳)
        - [Cell](#cell)
    - [Hbase基本结构(不完整版)](#hbase基本结构不完整版)
        - [1）Region Server(数据操作DML)](#1region-server数据操作dml)
        - [2）Master(表操作DDL)](#2master表操作ddl)
        - [3）Zookeeper](#3zookeeper)
        - [4）HDFS](#4hdfs)
    - [存储架构](#存储架构)
        - [Table](#table)

# Hbase基础

## 分布式知识

### 负载均衡

#### 负载均衡-客户端决定访问机器

客户端访问注册中心,拿到所有节点信息,由客户端决定访问那一个实例

#### 负载均衡-去中心化

redis中不存在注册中心,访问redis的任意节点就可以知道其他节点的信息,

## Hbase的特点

**1海量存储**

Hbase适合存储PB级别的海量数据，在PB级别的数据以及采用廉价PC存储的情况下，能在几十到百毫秒内返回数据。这与Hbase的极易扩展性息息相关。正式因为Hbase良好的扩展性，才为海量数据的存储提供了便利。

**2列式存储**

这里的列式存储其实说的是列族存储，Hbase是根据列族来存储数据的。列族下面可以有非常多的列，列族在创建表的时候就必须指定。

**3极易扩展**

Hbase的扩展性主要体现在两个方面，一个是基于上层处理能力（RegionServer）的扩展，一个是基于存储的扩展（HDFS）。
 通过横向添加RegionSever的机器，进行水平扩展，提升Hbase上层的处理能力，提升Hbsae服务更多Region的能力。

备注：RegionServer的作用是管理region、承接业务的访问，这个后面会详细的介绍通过横向添加Datanode的机器，进行存储层扩容，提升Hbase的数据存储能力和提升后端存储的读写能力。

**4高并发**

由于目前大部分使用Hbase的架构，都是采用的廉价PC，因此单个IO的延迟其实并不小，一般在几十到上百ms之间。这里说的高并发，主要是在并发的情况下，Hbase的单个IO延迟下降并不多。能获得高并发、低延迟的服务。

**5稀疏**

稀疏主要是针对Hbase列的灵活性，在列族中，你可以指定任意多的列，在列数据为空的情况下，是不会占用存储空间的。相对应的是密集表,比如mysql的表,即使某个cell内容为空,它依然占内存,但hbase为空的地方不占内存.

## Hbase存储设计与mysql对比

### mysql的水平垂直切分  与 列的动态扩展

#### 一条查询语句,mysql是从左向右查,oracle是从右向左查.

如果是select * from user_info where sex = "女" and old = 20;从左向右查的效率低,因为性别属性的区分度不高,会筛选出一半的数据来,再筛选年龄.

#### 宽表与高表

mysql是以行为单位,所以面临属性较多的宽表,应该做水平拆分

日志类的高表,应该做水平拆分.

#### 列的动态扩展

列动态扩展可以用某一列存json数据,再解析的办法:

#### mysql的弊端

mysql的存储引擎InnoDB ,当命名空间中所有表的数据上限达到64T,性能将大幅度下降

Isam 单表256T后 性能大幅度下降

### Hbase的解决方法

#### 列拆分:

列族信息用json来存储,然后把列拆成很多列族,比如basic列族下几个列,info下几个列

#### 行拆分:

水平拆分

#### 加缓存:

(**读写都放入缓存,交给缓存来异步入库**,因为写入的数据极大可能马上读取,所以不写入hbase,而是写入缓存)

为了防止缓存宕机,写入数据丢失,还要使用**顺序写入**来做**持久化**

#### 零拷贝:

#### Kafka的吞吐量高的原因:顺序写入+零拷贝+分段日志

00001.log  00001.index

01000.log   00010.index  log是分段日志,index对应日志文件里面,某一条数据所在的位置. 第1256条数据在01000.log里面,index则告诉了它在01000.log的哪个位置上.

# Hbase存储结构

## 逻辑结构

### Name Space

命名空间，类似于关系型数据库的 DatabBase 概念，每个命名空间下有多个表。HBase 有两个自带的命名空间，分别是 hbase 和 default，hbase 中存放的是 HBase 内置的表，

default 表是用户默认使用的命名空间。

### Row

HBase 表中的每行数据都由一个 RowKey 和多个 Column（列）组成，数据是按照 RowKey的字典顺序存储的，并且查询数据时只能根据 RowKey 进行检索，所以 RowKey 的设计十分重 要。

### Row Key

与nosql数据库们一样,row key是用来检索记录的主键。访问Hbase table中的行，只有三种方式：

1. 通过单个row key访问

2. 通过row key的range

3. 全表扫描

Row key行键 (Row key)可以是任意字符串(最大长度是 64KB，实际应用中长度一般为 10-100bytes)，在Hbase内部，row key保存为字节数组。

存储时，数据按照Row key的字典序(byte order)排序存储。设计key时，要充分排序存储这个特性，将经常一起读取的行存储放到一起。(位置相关性)

注意：

字典序对int排序的结果是1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。要保持整形的自然序，行键必须用0作左填充。

行的一次读写是原子操作 (不论一次读写多少列)。这个设计决策能够使用户很容易的理解程序在对同一个行进行并发更新操作时的行为。

### 列族

　　Hbase表中的每个列，都归属与某个列族。列族是表的chema的一部分(而列不是)，必须在使用表之前定义。列名都以列族作为前缀。例如courses:history，courses:math 都属于courses 这个列族。

　　访问控制、磁盘和内存的使用统计都是在列族层面进行的。实际应用中，列族上的控制权限能帮助我们管理不同类型的应用：我们允许一些应用可以添加 新的基本数据、一些应用可以读取基本数据并创建继承的列族、一些应用则只允许浏览数据(甚至可能因为隐私的原因不能浏览所有数据)。

#### Region

水平切分

类似于关系型数据库的表概念。不同的是，HBase 定义表时**只需要声明列族即可，不需要声明具体的列。**这意味着，往 HBase 写入数据时，字段可以动态、按需指定。因此，和关系型数据库相比，HBase 能够轻松应对字段变更的场景。

#### Store

水平+垂直切分

## 物理存储(store的实际存储方式)

store是一个逻辑结构,他的物理存储方式如下:

因为有时间戳的存在,所以数据的删除更新会在之后进行,用户读的时候返回最新时间戳数据即可,所以hbase的读写效率很快.

### Type

操作类型,增删查改

### 时间戳

windows和linux的时间要统一,不然会出错.

Hbase中通过row和columns确定的为一个存贮单元称为cell。每个 cell都保存着同一份数据的多个版本。版本通过时间戳来索引。时间戳的类型是 64位整型。时间戳可以由Hbase(在数据写入时自动 )赋值，此时时间戳是精确到毫秒的当前系统时间。时间戳也可以由客户显式赋值。如果应用程序要避免数据版本冲突，就必须自己生成具有唯一性的时间戳。每个 cell中，不同版本的数据按照时间倒序排序，即最新的数据排在最前面。

为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担，Hbase提供了两种数据版本回收方式。一是保存数据的最后n个版本，二是保存最近一段时间内的版本(比如最近七天)。用户可以针对每个列族进行设置。

### Cell

由{**rowkey, column Family：column Qualifier, time Stamp} 唯一确定的单元。**cell 中的数 据是没有类型的，**全部是字节码形式存贮。**

在HBase每个cell存储单元对同一份数据有多个版本，根据唯一的时间戳来区分每个版本之间的差异，时间戳可以由HBase(在数据写入时自动)赋值，此时时间戳是精确到毫秒的当前系统时间，也可以由自己显示指定，按照时间戳倒序排序，最新的数据排在最前面。

## Hbase基本结构(不完整版)

架构角色：

### 1）Region Server(数据操作DML)

Region Server 为 Region 的管理者，其实现类为 HRegionServer，主要作用如下:

对于数据的操作：get, put, delete；

对于 Region 的操作：splitRegion、compactRegion。

### 2）Master(表操作DDL)

Master 是所有 Region Server 的管理者，其实现类为 HMaster，主要作用如下：

对于表的操作：create, delete, alter

对于 RegionServer 的操作：分配 regions 到每个 RegionServer，监控每个 RegionServer 的状态，负载均衡和故障转移。

Hbase的master不需要配置,开多少台服务,就有多少个master高可用

### 3）Zookeeper

HBase 通过 Zookeeper 来做 Master 的高可用、RegionServer 的监控、元数据的入口以及

集群配置的维护等工作。

### 4）HDFS

HDFS 为 HBase 提供最终的底层数据存储服务，同时为 HBase 提供高可用的支持。

## 存储架构

### Table

Table中的所有行都按照row key的字典序排列，Table 在行的方向上分割为多个Hregion。

region按大小分割的，每个表一开始只有一个region，随着数据不断插入表，region不断增大，当增大到一个阀值的时候，Hregion就会等分会两个新的Hregion。当table中的行不断增多，就会有越来越多的Hregion。

# Mongodb与Hbase的区别

- 1.Mongodb bson文档型数据库，整个数据都存在磁盘中，hbase是列式数据库，集群部署时每个familycolumn保存在单独的hdfs文件中。
- 2.Mongodb 主键是“_id”,主键上面可以不建索引,记录插入的顺序和存放的顺序一样，hbase的主键就是row key，可以是任意字符串(最大长度是 64KB，实际应用中长度一般为 10-100bytes)，在hbase内部，**row key保存为字节数组。存储时，数据按照Row key的字典序(byte order)排序存储。设计key时，要充分排序存储这个特性，将经常一起读取的行存储放到一起。字典序对int排序的结果是1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。要保持整形的自然序，行键必须用0作左填充。**
- 3.Mongodb支持二级索引，而hbase本身不支持二级索引
- 4.Mongodb支持集合查找，正则查找，范围查找，支持skip和limit等等，是**最像mysql的nosql数据库**，而hbase只支持三种查找：**通过单个row key访问，通过row key的range，全表扫描**
- 5.mongodb的update是update-in-place，也就是原地更新，除非原地容纳不下更新后的数据记录。而hbase的修改和添加都是同一个命令：put，如果put传入的row key已经存在就更新原记录,实际上hbase内部也不是更新，它只是将这一份数据已不同的版本保存下来而已，hbase默认的保存版本的历史数量是3。
- 6.mongodb的delete会将该行的数据标示为已删除，因为mongodb在删除记录时并不是真把记录从内存或文件中remove，而是将该删除记录数据置空（写0或特殊数字加以标识）同时将该记录所在地址放到一个list列表“释放列表”中，这样做的好就是就是如果有用户要执行插入记录操作时，mongodb会首先从该“释放列表”中获取size合适的“已删除记录”地址返回，这种方法会提升性能（避免了malloc内存操作），同时mongodb也使用了bucket size数组来定义多个大小size不同的列表，用于将要删除的记录根据其size大小放到合适的“释放列表”中。Hbase的delete是先新建一个tombstonemarkers，然后读的时候会和tombstonemarkers做merge，在 发生major compaction时delete的数据记录才会真真删除。
- 7.mongodb和hbase都支持mapreduce，不过mongodb的mapreduce支持不够强大，如果没有使用mongodb分片，mapreduce实际上不是并行执行的
- 8.mongodb支持shard分片，hbase根据row key自动负载均衡，这里shard key和row key的选取尽量用非递增的字段，尽量用分布均衡的字段，因为分片都是根据范围来选择对应的存取server的，如果用递增字段很容易热点server的产生，由于是根据key的范围来自动分片的，如果key分布不均衡就会导致有些key根本就没法切分，从而产生负载不均衡。
- 9.mongodb的读效率比写高，hbase默认适合写多读少的情况，可以通过hfile.block.cache.size配置，该配置storefile的读缓存占用Heap的大小百分比，0.2表示20%。该值直接影响数据读的性能。如果写比读少很多，开到0.4-0.5也没问题。如果读写较均衡，0.3左右。如果写比读多，果断默认0.2吧。设置这个值的时候，你同时要参考hbase.regionserver.global.memstore.upperLimit，该值是memstore占heap的最大百分比，两个参数一个影响读，一个影响写。如果两值加起来超过80-90%，会有OOM的风险，谨慎设置。
- 10.hbase采用的LSM思想(Log-Structured Merge-Tree)，就是将对数据的更改hold在内存中，达到指定的threadhold后将该批更改merge后批量写入到磁盘，这样将单个写变成了批量写，大大提高了写入速度，不过这样的话读的时候就费劲了，需要merge disk上的数据和memory中的修改数据，这显然降低了读的性能。mongodb采用的是mapfile+Journal思想，如果记录不在内存，先加载到内存，然后在内存中更改后记录日志，然后隔一段时间批量的写入data文件，这样对内存的要求较高，至少需要容纳下热点数据和索引。

# OLAP与OLTP

mysql是oltp的sql数据库，是行存储。

hbase是oltp的nosql数据库，不是列存储而是KV存储。

但hbase的分析是行分析，所以并不满足olap的性能要求，所以用oss最为列存储的基石。

# 星型模型和雪花型模型比较

https://blog.csdn.net/nisjlvhudy/article/details/7889422

星型有冗余和数据重复，但效率高，用来做指标分析更适合，比如“给定的一个客户他们的收入是多少?”

雪花型拆分较细，效率低；但适合维度分析，比如“针对特定的广告主，有哪些客户或者公司是在线的?”

***\*星型架构是一种非正规化的结构，多维数据集的每一个维度都直接与事实表相连接，不存在渐变维度，所以数据有一定的冗余\****，如在地域维度表中，存在国家 A 省 B 的城市 C 以及国家 A 省 B 的城市 D 两条记录，那么国家 A 和省 B 的信息分别存储了两次，即存在冗余。

它的优点是 : ***\*通过最大限度地减少数据存储量以及联合较小的维表来改善查询性能\****

# 数据湖与数据仓库

## 数据湖与数据仓库的区别？

- 数据湖：数据湖存储OSS：允许您以任意规模存储所有结构化和非结构化数据。数据湖分析DLA：按原样存储数据（无需先对数据进行结构化处理），并运行不同类型的分析 – 从控制面板和可视化到大数据处理、实时分析和机器学习，以指导做出更好的决策。
- 数据仓库：是结构化的，借助索引提供更高性能的服务；一般情况下，数据湖分析DLA会跟 数据仓库 ADB 搭配，DLA会把分析与计算的数据写入到 ADB之中，以提供更高性能的实时分析的能力，客户会为此额外支付一笔费用；

<file_sep>/notes/操作系统.md

- [进程与线程](#进程与线程)
    - [1. 进程](#1-进程)
    - [2. 线程](#2-线程)
    - [3. 区别](#3-区别)
- [死锁的必要条件](#死锁的必要条件)
- [死锁预防](#死锁预防)
    - [1. 破坏互斥条件](#1-破坏互斥条件)
    - [2. 破坏占有和等待条件](#2-破坏占有和等待条件)
    - [3. 破坏不可抢占条件](#3-破坏不可抢占条件)
    - [4. 破坏环路等待](#4-破坏环路等待)

# 进程与线程

## 1. 进程

进程是资源分配的基本单位。

进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态，所谓的创建进程和撤销进程，都是指对 PCB 的操作。

下图显示了 4 个程序创建了 4 个进程，这 4 个进程可以并发地执行。

## 2. 线程

线程是独立调度的基本单位。

一个进程中可以有多个线程，它们共享进程资源。

QQ 和浏览器是两个进程，浏览器进程里面有很多线程，例如 HTTP 请求线程、事件响应线程、渲染线程等等，线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时，浏览器还可以响应用户的其它事件。

## 3. 区别

Ⅰ 拥有资源

进程是资源分配的基本单位，但是线程不拥有资源，多个线程共享进程的资源

Ⅱ 调度

线程是独立调度的基本单位，在同一进程中，线程的切换不会引起进程切换，从一个进程中的线程切换到另一个进程中的线程时，会引起进程切换。

Ⅲ 系统开销

由于创建或撤销进程时，系统都要为之分配或回收资源，如内存空间、I/O 设备等，所付出的开销远大于创建或撤销线程时的开销。类似地，在进行进程切换时，涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置，而线程切换时只需保存和设置少量寄存器内容，开销很小。

Ⅳ 通信方面

线程间可以通过直接读写同一进程中的数据进行通信，但是进程通信需要借助 IPC。

# 死锁的必要条件

- 互斥：每个资源要么已经分配给了一个进程，要么就是可用的。
- 占有和等待：已经得到了某个资源的进程可以再请求新的资源。
- 不可抢占：已经分配给一个进程的资源不能强制性地被抢占，它只能被占有它的进程显式地释放。
- 环路等待：有两个或者两个以上的进程组成一条环路，该环路中的每个进程都在等待下一个进程所占有的资源。

# 死锁预防

在程序运行之前预防发生死锁。

## 1. 破坏互斥条件

例如假脱机打印机技术允许若干个进程同时输出，唯一真正请求物理打印机的进程是打印机守护进程。

## 2. 破坏占有和等待条件

一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。

## 3. 破坏不可抢占条件

## 4. 破坏环路等待

给资源统一编号，进程只能按编号顺序来请求资源。<file_sep>/notes/分布式.md

- [一、分布式锁](#一分布式锁)
    - [数据库的唯一索引](#数据库的唯一索引)
    - [Redis 的 SETNX 指令](#redis-的-setnx-指令)
    - [Redis 的 RedLock 算法](#redis-的-redlock-算法)
    - [Zookeeper 的有序节点](#zookeeper-的有序节点)
        - [1. Zookeeper 抽象模型](#1-zookeeper-抽象模型)
        - [2. 节点类型](#2-节点类型)
        - [3. 监听器](#3-监听器)
        - [4. 分布式锁实现](#4-分布式锁实现)
        - [5. 会话超时](#5-会话超时)
        - [6. 羊群效应](#6-羊群效应)
- [二、分布式事务](#二分布式事务)
    - [2PC](#2pc)
        - [1. 运行过程](#1-运行过程)
            - [1.1 准备阶段](#11-准备阶段)
            - [1.2 提交阶段](#12-提交阶段)
        - [2. 存在的问题](#2-存在的问题)
            - [2.1 同步阻塞](#21-同步阻塞)
            - [2.2 单点问题](#22-单点问题)
            - [2.3 数据不一致](#23-数据不一致)
            - [2.4 太过保守](#24-太过保守)
    - [本地消息表](#本地消息表)
    - [MQ,Sage,TCC](#mqsagetcc)
- [三、CAP](#三cap)
    - [一致性](#一致性)
    - [可用性](#可用性)
    - [分区容忍性](#分区容忍性)
    - [权衡](#权衡)
- [四、BASE](#四base)
    - [基本可用](#基本可用)
    - [软状态](#软状态)
    - [最终一致性](#最终一致性)
- [五、Paxos](#五paxos)
    - [执行过程](#执行过程)
        - [1. Prepare 阶段](#1-prepare-阶段)
        - [2. Accept 阶段](#2-accept-阶段)
        - [3. Learn 阶段](#3-learn-阶段)
    - [约束条件](#约束条件)
        - [1\. 正确性](#1\-正确性)
        - [2\. 可终止性](#2\-可终止性)
- [六、Raft](#六raft)
    - [单个 Candidate 的竞选](#单个-candidate-的竞选)
    - [多个 Candidate 竞选](#多个-candidate-竞选)
    - [数据同步](#数据同步)

# 一、分布式锁

在单机场景下，可以使用语言的内置锁来实现进程同步。但是在分布式场景下，需要同步的进程可能位于不同的节点上，那么就需要使用分布式锁。

阻塞锁通常使用互斥量来实现：

- 互斥量为 0 表示有其它进程在使用锁，此时处于锁定状态；
- 互斥量为 1 表示未锁定状态。

1 和 0 可以用一个整型值表示，也可以用某个数据是否存在表示。

## 数据库的唯一索引

获得锁时向表中插入一条记录，释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次，那么就可以用这个记录是否存在来判断是否存于锁定状态。

存在以下几个问题：

- 锁没有失效时间，解锁失败的话其它进程无法再获得该锁。
- 只能是非阻塞锁，插入失败直接就报错了，无法重试。
- 不可重入，已经获得锁的进程也必须重新获取锁。

## Redis 的 SETNX 指令

使用 SETNX（set if not exist）指令插入一个键值对，如果 Key 已经存在，那么会返回 False，否则插入成功并返回 True。

SETNX 指令和数据库的唯一索引类似，保证了只存在一个 Key 的键值对，那么可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。

EXPIRE 指令可以为一个键值对设置一个过期时间，从而避免了数据库唯一索引实现方式中释放锁失败的问题。

## Redis 的 RedLock 算法

使用了多个 Redis 实例来实现分布式锁，这是为了保证在发生单点故障时仍然可用。

- 尝试从 N 个互相独立 Redis 实例获取锁；
- 计算获取锁消耗的时间，只有当这个时间小于锁的过期时间，并且从大多数（N / 2 + 1）实例上获取了锁，那么就认为锁获取成功了；
- 如果锁获取失败，就到每个实例上释放锁。

## Zookeeper 的有序节点

### 1. Zookeeper 抽象模型

Zookeeper 提供了一种树形结构的命名空间，/app1/p_1 节点的父节点为 /app1。

### 2. 节点类型

- 永久节点：不会因为会话结束或者超时而消失；
- 临时节点：如果会话结束或者超时就会消失；
- 有序节点：会在节点名的后面加一个数字后缀，并且是有序的，例如生成的有序节点为 /lock/node-0000000000，它的下一个有序节点则为 /lock/node-0000000001，以此类推。

### 3. 监听器

为一个节点注册监听器，在节点状态发生改变时，会给客户端发送消息。

### 4. 分布式锁实现

- 创建一个锁目录 /lock；
- 当一个客户端需要获取锁时，在 /lock 下创建临时的且有序的子节点；
-  客户端获取 /lock 下的子节点列表，判断自己创建的子节点是否为当前子节点列表中序号最小的子节点，如果是则认为获得锁；否则监听自己的前一个子节点，获得子节点的变更通知后重复此步骤直至获得锁；
- 执行业务代码，完成后，删除对应的子节点。

### 5. 会话超时

如果一个已经获得锁的会话超时了，因为创建的是临时节点，所以该会话对应的临时节点会被删除，其它会话就可以获得锁了。可以看到，Zookeeper 分布式锁不会出现数据库的唯一索引实现的分布式锁释放锁失败问题。

### 6. 羊群效应

一个节点未获得锁，只需要监听自己的前一个子节点，这是因为如果监听所有的子节点，那么任意一个子节点状态改变，其它所有子节点都会收到通知（羊群效应），而我们只希望它的后一个子节点收到通知。

# 二、分布式事务

指事务的操作位于不同的节点上，需要保证事务的 ACID 特性。

例如在下单场景下，库存和订单如果不在同一个节点上，就涉及分布式事务。

## 2PC

两阶段提交（Two-phase Commit，2PC），通过引入协调者（Coordinator）来协调参与者的行为，并最终决定这些参与者是否要真正执行事务。

### 1. 运行过程

#### 1.1 准备阶段

协调者询问参与者事务是否执行成功，参与者发回事务执行结果。

#### 1.2 提交阶段

如果事务在每个参与者上都执行成功，事务协调者发送通知让参与者提交事务；否则，协调者发送通知让参与者回滚事务。

需要注意的是，在准备阶段，参与者执行了事务，但是还未提交。只有在提交阶段接收到协调者发来的通知后，才进行提交或者回滚。

### 2. 存在的问题

#### 2.1 同步阻塞

所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态，无法进行其它操作。

#### 2.2 单点问题

协调者在 2PC 中起到非常大的作用，发生故障将会造成很大影响。特别是在阶段二发生故障，所有参与者会一直等待，无法完成其它操作。

#### 2.3 数据不一致

在阶段二，如果协调者只发送了部分 Commit 消息，此时网络发生异常，那么只有部分参与者接收到 Commit 消息，也就是说只有部分参与者提交了事务，使得系统数据不一致。

#### 2.4 太过保守

任意一个节点失败就会导致整个事务失败，没有完善的容错机制。

## 本地消息表

本地消息表与业务数据表处于同一个数据库中，这样就能利用本地事务来保证在对这两个表的操作满足事务特性，并且使用了消息队列来保证最终一致性。

1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息，本地事务能保证这个消息一定会被写入本地消息表中。
2. 之后将本地消息表中的消息转发到消息队列中，如果转发成功则将消息从本地消息表中删除，否则继续重新转发。
3. 在分布式事务操作的另一方从消息队列中读取一个消息，并执行消息中的操作。

## MQ,Sage,TCC

https://juejin.im/post/5b5a0bf9f265da0f6523913b#heading-18

# 三、CAP

分布式系统不可能同时满足一致性（C：Consistency）、可用性（A：Availability）和分区容忍性（P：Partition Tolerance），最多只能同时满足其中两项。

## 一致性

一致性指的是多个数据副本是否能保持一致的特性，在一致性的条件下，系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。

对系统的一个数据更新成功之后，如果所有用户都能够读取到最新的值，该系统就被认为具有强一致性。

## 可用性

可用性指分布式系统在面对各种异常时可以提供正常服务的能力，可以用系统可用时间占总时间的比值来衡量，4 个 9 的可用性表示系统 99.99% 的时间是可用的。

在可用性条件下，要求系统提供的服务一直处于可用的状态，对于用户的每一个操作请求总是能够在有限的时间内返回结果。

## 分区容忍性

系统分布在多台服务器上.

在分区容忍性条件下，分布式系统在遇到任何网络分区故障的时候，仍然需要能对外提供一致性和可用性的服务，除非是整个网络环境都发生了故障。

## 权衡

在分布式系统中，分区容忍性必不可少,因为分布式系统必须要分区,同时在分区的前提下满足一致性和可用性。因此，CAP 理论实际上是要在可用性和一致性之间做权衡。

可用性和一致性往往是冲突的，很难使它们同时满足。在多个节点之间进行数据同步时，

- 为了保证一致性（CP），不能访问未同步完成的节点，也就失去了部分可用性；
- 为了保证可用性（AP），允许读取所有节点的数据，但是数据可能不一致。

# 四、BASE

BASE 是基本可用（Basically Available）、软状态（Soft State）和最终一致性（Eventually Consistent）三个短语的缩写。

其核心思想是：

> 既是无法做到强一致性（Strong consistency），但每个应用都可以根据自身的业务特点，采用适当的方式来使系统达到最终一致性（Eventual consistency）。

基本可用:分布式系统在出现故障时，允许损失部分可用功能，保证核心功能可用。

软状态:允许系统中存在中间状态，这个状态不影响系统可用性，这里指的是CAP中的不一致。

最终一致:最终一致是指经过一段时间后，所有节点数据都将会达到一致。

BASE 理论是对 CAP 中一致性和可用性权衡的结果，它的核心思想是：即使无法做到强一致性，但每个应用都可以根据自身业务特点，采用适当的方式来使系统达到最终一致性。

BASE解决了CAP中理论没有网络延迟，在BASE中用软状态和最终一致，保证了延迟后的一致性。BASE和 ACID 是相反的，它完全不同于ACID的强一致性模型，而是通过牺牲强一致性来获得可用性，并允许数据在一段时间内是不一致的，但最终达到一致状态。

## 基本可用

指分布式系统在出现故障的时候，保证核心可用，允许损失部分可用性。

例如，电商在做促销时，为了保证购物系统的稳定性，部分消费者可能会被引导到一个降级的页面。

## 软状态

指允许系统中的数据存在中间状态，并认为该中间状态不会影响系统整体可用性，即允许系统不同节点的数据副本之间进行同步的过程存在时延。

## 最终一致性

最终一致性强调的是系统中所有的数据副本，在经过一段时间的同步后，最终能达到一致的状态。

ACID 要求强一致性，通常运用在传统的数据库系统上。而 BASE 要求最终一致性，通过牺牲强一致性来达到可用性，通常运用在大型分布式系统中。

在实际的分布式场景中，不同业务单元和组件对一致性的要求是不同的，因此 ACID 和 BASE 往往会结合在一起使用。

而在实际工程实践中，最终一致性分为5种：

### 因果一致性（Causal consistency）

因果一致性指的是：如果节点A在更新完某个数据后通知了节点B，那么节点B之后对该数据的访问和修改都是基于A更新后的值。于此同时，和节点A无因果关系的节点C的数据访问则没有这样的限制。

### 读己之所写（Read your writes）

读己之所写指的是：节点A更新一个数据后，它自身总是能访问到自身更新过的最新值，而不会看到旧值。其实也算一种因果一致性。

### 会话一致性（Session consistency）

会话一致性将对系统数据的访问过程框定在了一个会话当中：系统能保证在同一个有效的会话中实现 “读己之所写” 的一致性，也就是说，执行更新操作之后，客户端能够在同一个会话中始终读取到该数据项的最新值。

### 单调读一致性（Monotonic read consistency）

单调读一致性指的是：如果一个节点从系统中读取出一个数据项的某个值后，那么系统对于该节点后续的任何数据访问都不应该返回更旧的值。

### 单调写一致性（Monotonic write consistency）

单调写一致性指的是：一个系统要能够保证来自同一个节点的写操作被顺序的执行。

> 在实际的实践中，这5种系统往往会结合使用，以构建一个具有最终一致性的分布式系统。

实际上，不只是分布式系统使用最终一致性，关系型数据库在某个功能上，也是使用最终一致性的。比如备份，数据库的复制过程是需要时间的，这个复制过程中，业务读取到的值就是旧的。当然，最终还是达成了数据一致性。这也算是一个最终一致性的经典案例。

# 五、Paxos

用于达成共识性问题，即对多个节点产生的值，该算法能保证只选出唯一一个值。

主要有三类节点：

- 提议者（Proposer）：提议一个值；
- 接受者（Acceptor）：对每个提议进行投票；
- 告知者（Learner）：被告知投票的结果，不参与投票过程。

## 执行过程

规定一个提议包含两个字段：[n, v]，其中 n 为序号（具有唯一性），v 为提议值。

### 1. Prepare 阶段

下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程，每个 Proposer 都会向所有 Acceptor 发送 Prepare 请求。

当 Acceptor 接收到一个 Prepare 请求，包含的提议为 [n1, v1]，并且之前还未接收过 Prepare 请求，那么发送一个 Prepare 响应，设置当前接收到的提议为 [n1, v1]，并且保证以后不会再接受序号小于 n1 的提议。

如下图，Acceptor X 在收到 [n=2, v=8] 的 Prepare 请求时，由于之前没有接收过提议，因此就发送一个 [no previous] 的 Prepare 响应，设置当前接收到的提议为 [n=2, v=8]，并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。

如果 Acceptor 接收到一个 Prepare 请求，包含的提议为 [n2, v2]，并且之前已经接收过提议 [n1, v1]。如果 n1 > n2，那么就丢弃该提议请求；否则，发送 Prepare 响应，该 Prepare 响应包含之前已经接收过的提议 [n1, v1]，设置当前接收到的提议为 [n2, v2]，并且保证以后不会再接受序号小于 n2 的提议。

如下图，Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的 Prepare 请求，由于之前已经接收过 [n=4, v=5] 的提议，并且 n > 2，因此就抛弃该提议请求；Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的 Prepare 请求，因为之前接收到的提议为 [n=2, v=8]，并且 2 <= 4，因此就发送 [n=2, v=8] 的 Prepare 响应，设置当前接收到的提议为 [n=4, v=5]，并且保证以后不会再接受序号小于 4 的提议。Acceptor Y 类似。

### 2. Accept 阶段

**当一个 Proposer 接收到超过一半 Acceptor 的 Prepare 响应时，就可以发送 Accept 请求。**

**Proposer A 接收到两个 Prepare 响应之后，就发送 [n=2, v=8] Accept 请求。该 Accept 请求会被所有 Acceptor 丢弃，因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。**

Proposer B 过后也收到了两个 Prepare 响应，因此也开始发送 Accept 请求。需要注意的是，Accept 请求的 v 需要取它收到的最大提议编号对应的 v 值，也就是 8。因此它发送 [n=4, v=8] 的 Accept 请求。

### 3. Learn 阶段

Acceptor 接收到 Accept 请求时，如果序号大于等于该 Acceptor 承诺的最小序号，那么就发送 Learn 提议给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议，那么该提议的提议值就被 Paxos 选择出来。

## 约束条件

### 1\. 正确性

指只有一个提议值会生效。

因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收，并且 Acceptor 不会接受两个不同的提议，因此可以保证正确性。

### 2\. 可终止性

指最后总会有一个提议生效。

Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢，因此能够保证可终止性。

# 六、Raft

参考博客:https://www.cnblogs.com/xybaby/p/10124083.html

## 选举解释:

## 选举过程详解

   上面已经说过，如果follower在*election timeout*内没有收到来自leader的心跳，（也许此时还没有选出leader，大家都在等；也许leader挂了；也许只是leader与该follower之间网络故障），则会主动发起选举。步骤如下：

- 增加节点本地的 *current term* ，切换到candidate状态
- 投自己一票
- 并行给其他节点发送 *RequestVote RPCs*
- 等待其他节点的回复

   在这个过程中，根据来自其他节点的消息，可能出现三种结果

1. 收到majority的投票（含自己的一票），则赢得选举，成为leader
2. 被告知别人已当选，那么自行切换到follower
3. 一段时间内没有收到majority投票，则保持candidate状态，重新发出选举

   第一种情况，赢得了选举之后，新的leader会立刻给所有节点发消息，广而告之，避免其余节点触发新的选举。在这里，先回到投票者的视角，投票者如何决定是否给一个选举请求投票呢，有以下约束：

- 在任一任期内，单个节点最多只能投一票
- 候选人知道的信息不能比自己的少（这一部分，后面介绍log replication和safety的时候会详细介绍）
- first-come-first-served 先来先得

   第二种情况，比如有三个节点A B C。A B同时发起选举，而A的选举消息先到达C，C给A投了一票，当B的消息到达C时，已经不能满足上面提到的第一个约束，即C不会给B投票，而A和B显然都不会给对方投票。A胜出之后，会给B,C发心跳消息，节点B发现节点A的term不低于自己的term，知道有已经有Leader了，于是转换成follower。

   第三种情况，没有任何节点获得majority投票，比如下图这种情况：
![img](pictures/分布式/1089769-20181216202546810-1327167758.png)

   总共有四个节点，Node C、Node D同时成为了candidate，进入了term 4，但Node A投了NodeD一票，NodeB投了Node C一票，这就出现了平票 split vote的情况。这个时候大家都在等啊等，直到超时后重新发起选举。如果出现平票的情况，那么就延长了系统不可用的时间（没有leader是不能处理客户端写请求的），因此raft引入了randomized election timeouts来尽量避免平票情况。同时，leader-based 共识算法中，节点的数目都是奇数个，尽量保证majority的出现。

## 选举过程图解

### 单个 Candidate 的竞选

有三种节点：Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间，一般为 150ms\~300ms，如果在这个时间内没有收到 Leader 的心跳包，就会变成 Candidate，进入竞选阶段。

- 下图展示一个分布式系统的最初阶段，此时只有 Follower 没有 Leader。Node A 等待一个随机的竞选超时时间之后，没收到 Leader 发来的心跳包，因此进入竞选阶段。

- 此时 Node A 发送投票请求给其它所有节点。

- 其它节点会对请求进行回复，如果超过一半的节点回复了，那么该 Candidate 就会变成 Leader。

- 之后 Leader 会周期性地发送心跳包给 Follower，Follower 接收到心跳包，会重新开始计时。

### 多个 Candidate 竞选

- 如果有多个 Follower 成为 Candidate，并且所获得票数相同，那么就需要重新开始投票。例如下图中 Node B 和 Node D 都获得两票，需要重新开始投票。

- 由于每个节点设置的随机竞选超时时间不同，因此下一次再次出现多个 Candidate 并获得同样票数的概率很低。

## 数据同步详解

 当有了leader，系统应该进入对外工作期了。客户端的一切请求来发送到leader，leader来调度这些并发请求的顺序，并且保证leader与followers状态的一致性。raft中的做法是，将这些请求以及执行顺序告知followers。leader和followers以相同的顺序来执行这些请求，保证状态一致。

### Replicated state machines

   共识算法的实现一般是基于复制状态机（Replicated state machines），何为复制状态机：

> If two identical, **deterministic** processes begin in the same state and get the same inputs in the same order, they will produce the same output and end in the same state.

   简单来说：**相同的初识状态 + 相同的输入 = 相同的结束状态**。引文中有一个很重要的词`deterministic`，就是说不同节点要以相同且确定性的函数来处理输入，而不要引入一下不确定的值，比如本地时间等。如何保证所有节点 `get the same inputs in the same order`，使用replicated log是一个很不错的注意，log具有持久化、保序的特点，是大多数分布式系统的基石。

  因此，可以这么说，在raft中，leader将客户端请求（command）封装到一个个log entry，将这些log entries复制（replicate）到所有follower节点，然后大家按相同顺序应用（apply）log entry中的command，则状态肯定是一致的。

### 请求完整流程

  当系统（leader）收到一个来自客户端的写请求，到返回给客户端，整个过程从leader的视角来看会经历以下步骤：

- leader append log entry(添加修改日志)
- leader issue AppendEntries RPC in parallel(把日志发给follower)
- leader wait for majority response(得到大多数follower的回应)
- leader apply entry to state machine(leader自己的内容发生修改)
- leader reply to client(把修改后的状态返回给客户端)
- leader notify follower apply log(leader通知follower也修改)

## 数据同步图解

- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交，只是写入日志中。

- Leader 会把修改复制到所有 Follower。

- Leader 会等待大多数的 Follower 也进行了修改，然后才将修改提交。

- 此时 Leader 会通知的所有 Follower 让它们也提交修改，此时所有节点的值达成一致。

# 中台

马云受supercell启发,15年确立中台战略.

## supercell

（超级细胞），芬兰移动游戏巨头。拥有《[部落冲突](https://baike.baidu.com/item/部落冲突/1802644)》、《[卡通农场](https://baike.baidu.com/item/卡通农场/9351095)》、《[海岛奇兵](https://baike.baidu.com/item/海岛奇兵/13466468)》、《[皇室战争](https://baike.baidu.com/item/皇室战争/3434195)》和《[荒野乱斗](https://baike.baidu.com/item/荒野乱斗/22072498)》 [1] 等全球热门游戏。

## 中台举例

公司刚开始只有淘宝，后来意识到B2C模式的业务也会是电商领域重要的组成部门，所以出现了天猫，随着天猫的不断发展，逐渐独立成一个部门，但是这两套都包含订单、商品、库存、价格、仓储、物流等基本业务系统。这两个系统互相独立，各自运行。

等到10年左右，阿里开始上线1688、聚划算等业务的时候发现，这些业务针对的领域虽然各不相同，但是他需要用到的系统功能也高度类似，主要也是订单、商品、库存、价格、仓储、物流等系统。如果这些新业务的系统也都要全部重新开发一遍，这无疑是很大的资源浪费。明明既有的系统调整一下就可以满足新业务的需求，为什么还要继续开发新系统呐？

在这个大的背景之下，阿里内部将共享服务部的职权不断提升，统一将各个业务业务部门重复使用，反复建设的功能和系统统一规划和管理。

![](https://pic3.zhimg.com/50/v2-7440efc0ee5430b973f66e6bdb5d8f2f_hd.jpg)

# 拜占庭将军问题

## 问题介绍

拜占庭帝国即中世纪的土耳其，拥有巨大的财富，周围10个邻邦垂诞已久，但拜占庭高墙耸立，固若金汤，没有一个单独的邻邦能够成功入侵。任何单个邻邦入侵的都会失败，同时也有可能自身被其他9个邻邦入侵。拜占庭帝国防御能力如此之强，至少要有十个邻邦中的一半以上同时进攻，才有可能攻破。

然而，如果其中的一个或者几个邻邦本身答应好一起进攻，但实际过程出现背叛，那么入侵者可能都会被歼灭。

于是每一方都小心行事，不敢轻易相信邻国。这就是拜占庭将军问题

## 区块链技术

互联网的存在，首先降低了信息的流通成本。每个将军配一台电脑，就解决了”书面协议“中骑马通讯造成时间延迟的问题。

如果10个将军中的几个同时发起消息，势必会造成系统的混乱，造成各说各的攻击时间方案，行动难以一致。

谁都可以发起进攻的信息，但由谁来发出呢？中本聪巧妙地在个系统加入了**发送信息的成本**，即：一段时间内只有一个节点可以传播信息。

它加入的成本就是”工作量“——节点必须完成一个计算工作才能向各城邦传播消息，当然，谁第一个完成工作，谁才能传播消息。

当某个节点发出统一进攻的消息后，各个节点收到发起者的消息必须签名盖章，确认各自的身份。中本聪在这里引用现代加密技术为这个信息签名。

这种加密技术——**非对称加密**完全可以解决古代难以解决的签名问题：

- 消息传送的私密性
- 能够确认身份
- 签名不可伪造、篡改

非对称加密算法的加密和解密使用不同的两个密钥.这两个密钥就是我们经常听到的"公开密钥"(公钥)和"私有密钥"(私钥).

公钥和私钥一般成对出现, 如果消息使用公钥加密,那么需要该公钥对应的私钥才能解密; 同样，如果消息使用私钥加密,那么需要该私钥对应的公钥才能解密.

非对称加密的作用是:**保护消息内容, 并且让消息接收方确定发送方的身份.**

比如，将军A想给将军B发送消息，为防止消息泄露，将军A只需要使用B的公钥对信息加密，而B的公钥是公开的，B只需要用只有他自己只的私钥解密即可。

将军B想要在信件上声明自己的身份，他可以自己写一段”签名文本“，并用私钥签名，并广播出去，所有人可以根据B的公钥来验证该签名，确定的B的身份。

由此，一个**不可信的分布式网络变成了一个可信的网络**，所有的参与者可以在某件事在达成一致。

写到这里，同时终于明白了工作量证明（Proof Of Work）的意义。有人说挖矿浪费了巨大的社会资源，但建立信任的成本可不是0，挖矿是维护比特币网络可靠性的最好办法。

工作量证明，简单的理解就是一份证明，现实中的毕业证、驾驶证都属于工作量证明，它用以检验结果的方式证明你过去所做过了多少工作。

在拜占庭的系统里，加入工作量证明，其实就是简单粗暴地引入了一个条件：**大家都别忙着发起消息，都来做个题，看谁最聪明，谁就有资格第一个发起消息。**

这个题必须是绝对公平的，中本聪在设计比特币时，它采用了一种工作量证明机制叫**哈希现金**，在一个交易块这要找到一个随机数，计算机只能用穷举法来找到这个随机数，可以说，能不能找到全靠运气，所以对于各个节点来说，这个世界上，只有随机才是真正的公平，实现随机的最好办法是使用数学，所有的将军在寻找共识的过程，借助了大家都认可的数学逻辑。

如果不同的将军先后解出了题，各自先后向这个网络发布消息，于是各个节点都会收到来自不同节点发起的进攻或者不进攻的消息，那怎么办的？只有时间最早的发起者才是有效的。中本聪巧妙的设计了一个**时间戳**的东西，为每个将军在解好题的时间（出块时间）盖上时间印章。

将军们那又凭什么要一起做工作量证明呢？中本聪也完全可以设置一个奖励机制，比特币的奖励机制是每打包一个块，目前是奖励25个比特币，当然，拜占庭将军问题的奖励机制可以是瓜分拜占庭获得的利益。

对了，如果有出现背叛怎么办？

在这个分布式网络里：

- 每个将军都有一份实时与其他将军同步的消息账本。
- 账本里有每个将军的签名都是可以验证身份的。如果有哪些消息不一致，可以知道消息不一致的是哪些将军。
- 尽管有消息不一致的，只要超过半数同意进攻，少数服从多数，共识达成。

由此，在一个分布式的系统中，尽管有坏人，坏人可以做任意事情（不受protocol限制），比如不响应、发送错误信息、对不同节点发送不同决定、不同错误节点联合起来干坏事等等。但是，只要大多数人是好人，就完全有可能去中心化地实现**共识**（Consensus）。

区块链上的共识机制主要解决由谁来构造区块，以及如何维护区块链统一的问题。

拜占庭容错问题需要解决的也同样是谁来发起信息，如何实现信息的统一同步的问题。

到这里也可以知道了，基于互联网的区块链技术，它克服了口头协议与书面协议的种种缺点，使用消息加密技术、以及公平的工作量证明机制，创建了一组所有将军都认可的协议，这套协议的出现，拜占庭将军问题也就完美的得到了解决。

<file_sep>/notes/LeetCode01.md

- [[617. 合并二叉树](https://leetcode-cn.com/problems/merge-two-binary-trees/)](#617-合并二叉树httpsleetcode-cncomproblemsmerge-two-binary-trees)
- [[@538. 把二叉搜索树转换为累加树](https://leetcode-cn.com/problems/convert-bst-to-greater-tree/)](#538-把二叉搜索树转换为累加树httpsleetcode-cncomproblemsconvert-bst-to-greater-tree)
- [[226. 翻转二叉树](https://leetcode-cn.com/problems/invert-binary-tree/)](#226-翻转二叉树httpsleetcode-cncomproblemsinvert-binary-tree)
- [[@104. 二叉树的最大深度](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/)](#104-二叉树的最大深度httpsleetcode-cncomproblemsmaximum-depth-of-binary-tree)
- [[@136. 只出现一次的数字](https://leetcode-cn.com/problems/single-number/)](#136-只出现一次的数字httpsleetcode-cncomproblemssingle-number)
- [[169. 多数元素](https://leetcode-cn.com/problems/majority-element/)](#169-多数元素httpsleetcode-cncomproblemsmajority-element)
- [[21. 合并两个有序链表](https://leetcode-cn.com/problems/merge-two-sorted-lists/)](#21-合并两个有序链表httpsleetcode-cncomproblemsmerge-two-sorted-lists)
- [[283. 移动零](https://leetcode-cn.com/problems/move-zeroes/)](#283-移动零httpsleetcode-cncomproblemsmove-zeroes)
- [[448. 找到所有数组中消失的数字](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/)](#448-找到所有数组中消失的数字httpsleetcode-cncomproblemsfind-all-numbers-disappeared-in-an-array)
- [[160. 相交链表](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/)](#160-相交链表httpsleetcode-cncomproblemsintersection-of-two-linked-lists)
- [[53. 最大子序和](https://leetcode-cn.com/problems/maximum-subarray/)](#53-最大子序和httpsleetcode-cncomproblemsmaximum-subarray)
- [[155. 最小栈](https://leetcode-cn.com/problems/min-stack/)](#155-最小栈httpsleetcode-cncomproblemsmin-stack)
- [[1. 两数之和](https://leetcode-cn.com/problems/two-sum/)](#1-两数之和httpsleetcode-cncomproblemstwo-sum)
- [[141. 环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)](#141-环形链表httpsleetcode-cncomproblemslinked-list-cycle)
- [@[234. 回文链表](https://leetcode-cn.com/problems/palindrome-linked-list/)](#234-回文链表httpsleetcode-cncomproblemspalindrome-linked-list)
- [[@20. 有效的括号](https://leetcode-cn.com/problems/valid-parentheses/)](#20-有效的括号httpsleetcode-cncomproblemsvalid-parentheses)
- [@[39. 组合总和](https://leetcode-cn.com/problems/combination-sum/)](#39-组合总和httpsleetcode-cncomproblemscombination-sum)
- [[22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/)](#22-括号生成httpsleetcode-cncomproblemsgenerate-parentheses)
- [[105. 从前序与中序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)](#105-从前序与中序遍历序列构造二叉树httpsleetcode-cncomproblemsconstruct-binary-tree-from-preorder-and-inorder-traversal)
- [[102. 二叉树的层次遍历](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/)](#102-二叉树的层次遍历httpsleetcode-cncomproblemsbinary-tree-level-order-traversal)
- [[347. 前 K 个高频元素](https://leetcode-cn.com/problems/top-k-frequent-elements/)](#347-前-k-个高频元素httpsleetcode-cncomproblemstop-k-frequent-elements)
- [[17. 电话号码的字母组合](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/)](#17-电话号码的字母组合httpsleetcode-cncomproblemsletter-combinations-of-a-phone-number)
- [[4. 寻找两个有序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/)](#4-寻找两个有序数组的中位数httpsleetcode-cncomproblemsmedian-of-two-sorted-arrays)
- [要再刷一遍](#要再刷一遍)
- [[46. 全排列](https://leetcode-cn.com/problems/permutations/)](#46-全排列httpsleetcode-cncomproblemspermutations)
- [[78. 子集](https://leetcode-cn.com/problems/subsets/)](#78-子集httpsleetcode-cncomproblemssubsets)
- [[@338. 比特位计数](https://leetcode-cn.com/problems/counting-bits/)](#338-比特位计数httpsleetcode-cncomproblemscounting-bits)
- [543. [二叉树的直径](https://leetcode-cn.com/problems/diameter-of-binary-tree/)](#543-二叉树的直径httpsleetcode-cncomproblemsdiameter-of-binary-tree)
- [@[96. 不同的二叉搜索树](https://leetcode-cn.com/problems/unique-binary-search-trees/)](#96-不同的二叉搜索树httpsleetcode-cncomproblemsunique-binary-search-trees)
- [[64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/)](#64-最小路径和httpsleetcode-cncomproblemsminimum-path-sum)
- [[437. 路径总和 III](https://leetcode-cn.com/problems/path-sum-iii/)](#437-路径总和-iiihttpsleetcode-cncomproblemspath-sum-iii)
- [[238. 除自身以外数组的乘积](https://leetcode-cn.com/problems/product-of-array-except-self/)](#238-除自身以外数组的乘积httpsleetcode-cncomproblemsproduct-of-array-except-self)
- [[48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/)](#48-旋转图像httpsleetcode-cncomproblemsrotate-image)
- [[49. 字母异位词分组](https://leetcode-cn.com/problems/group-anagrams/)](#49-字母异位词分组httpsleetcode-cncomproblemsgroup-anagrams)
- [[@739. 每日温度](https://leetcode-cn.com/problems/daily-temperatures/)](#739-每日温度httpsleetcode-cncomproblemsdaily-temperatures)
- [[@406. 根据身高重建队列](https://leetcode-cn.com/problems/queue-reconstruction-by-height/)](#406-根据身高重建队列httpsleetcode-cncomproblemsqueue-reconstruction-by-height)
- [[198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/)](#198-打家劫舍httpsleetcode-cncomproblemshouse-robber)
- [[@279. 完全平方数](https://leetcode-cn.com/problems/perfect-squares/)](#279-完全平方数httpsleetcode-cncomproblemsperfect-squares)
- [[75. 颜色分类](https://leetcode-cn.com/problems/sort-colors/)](#75-颜色分类httpsleetcode-cncomproblemssort-colors)
- [[142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/)](#142-环形链表-iihttpsleetcode-cncomproblemslinked-list-cycle-ii)
- [[@@394. 字符串解码](https://leetcode-cn.com/problems/decode-string/)](#394-字符串解码httpsleetcode-cncomproblemsdecode-string)
- [@@@@@动态规划类题目的反思](#动态规划类题目的反思)
- [[@@337. 打家劫舍 III](https://leetcode-cn.com/problems/house-robber-iii/)](#337-打家劫舍-iiihttpsleetcode-cncomproblemshouse-robber-iii)
- [[@@121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)](#121-买卖股票的最佳时机httpsleetcode-cncomproblemsbest-time-to-buy-and-sell-stock)
- [[122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/)](#122-买卖股票的最佳时机-iihttpsleetcode-cncomproblemsbest-time-to-buy-and-sell-stock-ii)
- [[@@309. 最佳买卖股票时机含冷冻期](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/)](#309-最佳买卖股票时机含冷冻期httpsleetcode-cncomproblemsbest-time-to-buy-and-sell-stock-with-cooldown)
- [[@@123. 买卖股票的最佳时机 III](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/)](#123-买卖股票的最佳时机-iiihttpsleetcode-cncomproblemsbest-time-to-buy-and-sell-stock-iii)
- [[@621. 任务调度器](https://leetcode-cn.com/problems/task-scheduler/)](#621-任务调度器httpsleetcode-cncomproblemstask-scheduler)
- [[@@146. LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/)](#146-lru缓存机制httpsleetcode-cncomproblemslru-cache)
- [[@@@416. 分割等和子集](https://leetcode-cn.com/problems/partition-equal-subset-sum/)](#416-分割等和子集httpsleetcode-cncomproblemspartition-equal-subset-sum)
- [[560. 和为K的子数组](https://leetcode-cn.com/problems/subarray-sum-equals-k/)](#560-和为k的子数组httpsleetcode-cncomproblemssubarray-sum-equals-k)
- [[@221. 最大正方形](https://leetcode-cn.com/problems/maximal-square/)](#221-最大正方形httpsleetcode-cncomproblemsmaximal-square)
- [[@34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)](#34-在排序数组中查找元素的第一个和最后一个位置httpsleetcode-cncomproblemsfind-first-and-last-position-of-element-in-sorted-array)
- [[240. 搜索二维矩阵 II](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/)](#240-搜索二维矩阵-iihttpsleetcode-cncomproblemssearch-a-2d-matrix-ii)
- [[@79. 单词搜索](https://leetcode-cn.com/problems/word-search/)](#79-单词搜索httpsleetcode-cncomproblemsword-search)
- [[19. 删除链表的倒数第N个节点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/)](#19-删除链表的倒数第n个节点httpsleetcode-cncomproblemsremove-nth-node-from-end-of-list)
- [[322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/)](#322-零钱兑换httpsleetcode-cncomproblemscoin-change)
- [[@@@152. 乘积最大子序列](https://leetcode-cn.com/problems/maximum-product-subarray/)](#152-乘积最大子序列httpsleetcode-cncomproblemsmaximum-product-subarray)
- [[2. 两数相加](https://leetcode-cn.com/problems/add-two-numbers/)](#2-两数相加httpsleetcode-cncomproblemsadd-two-numbers)
- [[98. 验证二叉搜索树](https://leetcode-cn.com/problems/validate-binary-search-tree/)](#98-验证二叉搜索树httpsleetcode-cncomproblemsvalidate-binary-search-tree)
- [[@@31. 下一个排列](https://leetcode-cn.com/problems/next-permutation/)](#31-下一个排列httpsleetcode-cncomproblemsnext-permutation)
- [背包问题的理解](#背包问题的理解)
- [@@@@@01背包](#01背包)
- [完全背包](#完全背包)
- [多重背包](#多重背包)
- [[@@33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/)](#33-搜索旋转排序数组httpsleetcode-cncomproblemssearch-in-rotated-sorted-array)
- [[@@3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)](#3-无重复字符的最长子串httpsleetcode-cncomproblemslongest-substring-without-repeating-characters)
- [[@56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/)](#56-合并区间httpsleetcode-cncomproblemsmerge-intervals)
- [[@@@300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)](#300-最长上升子序列httpsleetcode-cncomproblemslongest-increasing-subsequence)
- [[@@@@@128. 最长连续序列](https://leetcode-cn.com/problems/longest-consecutive-sequence/)](#128-最长连续序列httpsleetcode-cncomproblemslongest-consecutive-sequence)
- [[@23. 合并K个排序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/)](#23-合并k个排序链表httpsleetcode-cncomproblemsmerge-k-sorted-lists)
- [[124. 二叉树中的最大路径和](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/)](#124-二叉树中的最大路径和httpsleetcode-cncomproblemsbinary-tree-maximum-path-sum)
- [[@@@239. 滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/)](#239-滑动窗口最大值httpsleetcode-cncomproblemssliding-window-maximum)
- [[@11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/)](#11-盛最多水的容器httpsleetcode-cncomproblemscontainer-with-most-water)
- [[@@42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/)](#42-接雨水httpsleetcode-cncomproblemstrapping-rain-water)
- [[@@@407. 接雨水 II](https://leetcode-cn.com/problems/trapping-rain-water-ii/)](#407-接雨水-iihttpsleetcode-cncomproblemstrapping-rain-water-ii)
- [[@@@84. 柱状图中最大的矩形](https://leetcode-cn.com/problems/largest-rectangle-in-histogram/)](#84-柱状图中最大的矩形httpsleetcode-cncomproblemslargest-rectangle-in-histogram)
- [[@@76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/)](#76-最小覆盖子串httpsleetcode-cncomproblemsminimum-window-substring)
- [[@@@438. 找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/)](#438-找到字符串中所有字母异位词httpsleetcode-cncomproblemsfind-all-anagrams-in-a-string)
- [[200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/)](#200-岛屿数量httpsleetcode-cncomproblemsnumber-of-islands)
- [[@@@@139. 单词拆分](https://leetcode-cn.com/problems/word-break/)](#139-单词拆分httpsleetcode-cncomproblemsword-break)
- [[55. 跳跃游戏](https://leetcode-cn.com/problems/jump-game/)](#55-跳跃游戏httpsleetcode-cncomproblemsjump-game)
- [[@@662. 二叉树最大宽度](https://leetcode-cn.com/problems/maximum-width-of-binary-tree/)](#662-二叉树最大宽度httpsleetcode-cncomproblemsmaximum-width-of-binary-tree)
- [[@15. 三数之和](https://leetcode-cn.com/problems/3sum/)](#15-三数之和httpsleetcode-cncomproblems3sum)
- [@归并排序(mergeSort)](#归并排序mergesort)
- [@快速排序(quickSort)](#快速排序quicksort)
- [@@@堆排序](#堆排序)
- [@@但凡是有关left,right的函数,一般要判断left>right return](#但凡是有关leftright的函数一般要判断leftright-return)
- [[面试题42. 连续子数组的最大和](https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/)](#面试题42-连续子数组的最大和httpsleetcode-cncomproblemslian-xu-zi-shu-zu-de-zui-da-he-lcof)
- [[面试题03. 数组中重复的数字](https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/)](#面试题03-数组中重复的数字httpsleetcode-cncomproblemsshu-zu-zhong-zhong-fu-de-shu-zi-lcof)
- [[@@@@32. 最长有效括号](https://leetcode-cn.com/problems/longest-valid-parentheses/)](#32-最长有效括号httpsleetcode-cncomproblemslongest-valid-parentheses)
- [[@935. 骑士拨号器](https://leetcode-cn.com/problems/knight-dialer/)](#935-骑士拨号器httpsleetcode-cncomproblemsknight-dialer)
- [[@@1353. 最多可以参加的会议数目](https://leetcode-cn.com/problems/maximum-number-of-events-that-can-be-attended/)](#1353-最多可以参加的会议数目httpsleetcode-cncomproblemsmaximum-number-of-events-that-can-be-attended)
- [[@25. K 个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/)](#25-k-个一组翻转链表httpsleetcode-cncomproblemsreverse-nodes-in-k-group)
- [[@@@721. 账户合并](https://leetcode-cn.com/problems/accounts-merge/)](#721-账户合并httpsleetcode-cncomproblemsaccounts-merge)
- [[@@@301. 删除无效的括号](https://leetcode-cn.com/problems/remove-invalid-parentheses/)](#301-删除无效的括号httpsleetcode-cncomproblemsremove-invalid-parentheses)

#### [617. 合并二叉树](https://leetcode-cn.com/problems/merge-two-binary-trees/)

```java
改进:
当其中一颗树为空了之后,要学会剪枝

public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
        TreeNode root;
        if (t1 != null && t2 != null) {
            root = new TreeNode(t1.val + t2.val);
            root.left = mergeTrees(t1.left, t2.left);
            root.right = mergeTrees(t1.right, t2.right);
        } else if (t1 == null && t2 == null) {
            return null;
        } else if (t1 == null) {
            return t2;
        } else {
            return t1;
            //下面的可以剪枝
           // root = new TreeNode(t1.val);
            // root.left = mergeTrees(t1.left,null);
            // root.right = mergeTrees(t1.right,null);
        }
        return root;
    }

改进:非迭代的先序遍历 while(p!=null&&!s.isEmpty()){  在这里访问节点 while(p!=null)p = p.left; }
    中序遍历 while(p!=null&&!s.isEmpty()){   while(p!=null)p = p.left;   } 在后续pop出来的时候访问
	后序遍历比较麻烦.懒得记

     public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
        if (t1 != null && t2 != null) {
            stack.push(t2);
            stack.push(t1);
        } else {
            return t1 == null ? t2 : t1;
        }
        while (!stack.isEmpty()) {
            TreeNode s1 = stack.pop();
            TreeNode s2 = stack.pop();
            s1.val = s1.val + s2.val;
            if (s1.left != null && s2.left != null) {
                stack.push(s2.left);
                stack.push(s1.left);
            } else if (s2.left != null) {
                s1.left = s2.left;
            }
            if (s1.right != null && s2.right != null) {
                stack.push(s2.right);
                stack.push(s1.right);

            } else if (s2.right != null) {
                s1.right = s2.right;
            }
        }
        return t1;
    }

```

#### [@538. 把二叉搜索树转换为累加树](https://leetcode-cn.com/problems/convert-bst-to-greater-tree/)

@用迭代来做一下试试

```java
改进:树的非递归中序遍历
    树的非递归前序遍历与中序遍历的区别是,前序遍历在压栈的时候做节点数据访问;
	树的非递归前序遍历还可以用深度优先搜索的写法,详情看226题翻转二叉树.
//非递归中序遍历变式 (右,根,左)
    private Stack<TreeNode> s = new Stack<TreeNode>();
    private int sum = 0;

    public TreeNode convertBST(TreeNode root) {
        if (root == null)
            return null;
        TreeNode p = root;
        while (!s.isEmpty() || p != null) {
            while (p != null) {
                //前序遍历访问数据处理
                s.push(p);
                p = p.right;
            }
            if (!s.isEmpty()) {
                p = s.pop();
                //中序遍历访问数据处
                sum += p.val;
                p.val = sum;
            }
            p = p.left;
        }
        return root;
    }
```

#### [226. 翻转二叉树](https://leetcode-cn.com/problems/invert-binary-tree/)

```java
深度优先搜索相当于先序遍历,
广度优先搜索相当于层次遍历.

 //迭代,先序遍历,
    private Stack<TreeNode> s = new Stack<TreeNode>();

    public TreeNode invertTree(TreeNode root) {
        if (root == null)
            return null;
        TreeNode p = root;
        while (!s.isEmpty() || p != null) {
            while (p != null) {
                TreeNode temp = p.right;
                p.right = p.left;
                p.left = temp;

                s.push(p);
                p = p.left;
            }
            if (!s.isEmpty()) {
                p = s.pop();
            }
            p = p.right;
        }
        return root;
    }
```

#### [@104. 二叉树的最大深度](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/)

```java
改进:非递归中序遍历

 private int maxDeep = 0;

    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        Stack<DeepTreeNode> stack = new Stack<>();
        TreeNode p = root;
        int deep = 1;
        DeepTreeNode deepNode = new DeepTreeNode(root, 1);
        while (!stack.isEmpty() || p != null) {
            while (p != null) {
                stack.push(new DeepTreeNode(p, deep++));
                p = p.left;
            }
            if (!stack.isEmpty()) {
                DeepTreeNode temp = stack.pop();
                p = temp.node;
                deep = temp.deep;
                maxDeep = Math.max(maxDeep, deep);
            }
            p = p.right;
            deep = deep + 1;
        }
        return maxDeep;
    }

树的深度优先遍历,也是树的第二种迭代先序遍历

private Stack<Pair> s = new Stack<Pair>();
    private int maxDeep = 0;

    public int maxDepth(TreeNode root) {
        if (root == null)
            return maxDeep;
        s.push(new Pair(1, root));
        maxDeep = 1;

        while (!s.isEmpty()) {
            Pair p = s.pop();
            int curDeep = p.deepth;
            maxDeep = Math.max(curDeep, maxDeep);
            TreeNode node = p.node;
            if (node.left != null)
                s.push(new Pair(curDeep + 1, node.left));
            if (node.right != null)
                s.push(new Pair(curDeep + 1, node.right));
        }
        return maxDeep;
    }

    class Pair {
        int deepth;
        TreeNode node;

        Pair(int deepth, TreeNode node) {
            this.deepth = deepth;
            this.node = node;
        }
    }
```

#### [@136. 只出现一次的数字](https://leetcode-cn.com/problems/single-number/)

@异或的办法

```java
思路
1.求差  2(a+b+c) - (a+a+b+b+c)   第一步要求不重复,应该想到用set,Iterator<Integer> i = s.iterator();
2.异或   sum = temp^sum
3.hashMap第一次出现存到map,第二次出现从map里面删除掉
```

#### [169. 多数元素](https://leetcode-cn.com/problems/majority-element/)

```java
public int majorityElement(int[] nums) {
        if (nums == null || nums.length == 0)
            return -1;
        int maxNum = nums[0];
        int count = 1;
        for (int temp : nums) {
            if (temp == maxNum) {
                count++;
            } else {
                count--;
                if (count == 0) {
                    maxNum = temp;
                    count = 1;
                }
            }
        }
        count = 0;
        for (int temp : nums) {
            if (maxNum == temp)
                count++;
        }
        if (count > nums.length / 2)
            return maxNum;
        else
            return -1;
    }
```

#### [21. 合并两个有序链表](https://leetcode-cn.com/problems/merge-two-sorted-lists/)

```java
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode last = new ListNode(-1);
        ListNode pHead = last;
        while (l1 != null || l2 != null) {

            if (l1 == null && l2 != null) {
                l1 = l2;
                l2 = null;
            }
            while (l2 == null && l1 != null) {
                ListNode next = l1.next;
                l1.next = last.next;
                last.next = l1;
                l1 = next;
                last = last.next;
            }
            while (l1 != null && l2 != null) {
                ListNode min = null;
                if (l1.val <= l2.val) {
                    min = l1;
                    l1 = l1.next;
                } else {
                    min = l2;
                    l2 = l2.next;
                }
                min.next = last.next;
                last.next = min;
                last = last.next;
            }
        }
        return pHead.next;
    }
```

#### [283. 移动零](https://leetcode-cn.com/problems/move-zeroes/)

```java
改进:把前面index个位置填满正确的数,最后把index后面的位用0填充
public void moveZeroes(int[] nums) {
        int index = 0;
        for(int temp:nums){
            if(temp!=0){
                nums[index++] = temp;
            }
        }
        for(int i =index;i<nums.length;i++){
            nums[i] = 0;
        }
    }
```

#### [448. 找到所有数组中消失的数字](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/)

```java
改进:
一种意外情况需要考虑   数列: 3,2,3  i=0的时候的3应该放在下标为2的地方,也就是第二个3所在的地方,
但如果一直交换的话,会陷入死循环,所以判断条件里加了一句   nums[i] != nums[index]  ,意思是i所
在位置的值num[i],在index(num[i]-1]处已经存在值num[i]-1了.这个时候不做交换,直接跳过.

  具体来说就是  i=0处 的3应该放在index=2处,但index=2处已经有3了,所以i++,继续往后计算.

public List<Integer> findDisappearedNumbers(int[] nums) {
        ArrayList<Integer> ret = new ArrayList<Integer>();
        for (int i = 0; i < nums.length; ) {
            int index = nums[i] - 1;
            if (index != i && nums[i] != nums[index]) {
                int temp = nums[index];
                nums[index] = nums[i];
                nums[i] = temp;
            } else {
                i++;
            }
        }
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] - 1 != i) {
                ret.add(i + 1);
            }
        }
        return ret;
    }

方法二:                                                                                HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
    public List<Integer> findDisappearedNumbers(int[] nums) {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i], 1);
        }
        for (int j = 1; j <= nums.length; j++) {
            if (map.get(j) == null) {
                list.add(j);
            }
        }
        return list;
    }

                                                                                改进                                                                                关于map的遍历                                                                                Set<Map.Entry<Integer,Integer>> set = map.entrySet();                                                                                Iterator<Map.Entry<Integer,Integer>> it = set.iterator();

```

#### [160. 相交链表](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/)

```java
 public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA = 0;
        int lenB = 0;
        ListNode pA = headA;
        ListNode pB = headB;
        while (pA != null) {
            lenA++;
            pA = pA.next;
        }
        while (pB != null) {
            lenB++;
            pB = pB.next;
        }
        if (lenB > lenA) {
            int temp = lenA;
            lenA = lenB;
            lenB = temp;
            pA = headB;
            pB = headA;
        } else {
            pA = headA;
            pB = headB;
        }
        for (int i = 1; i <= lenA - lenB; i++) {
            pA = pA.next;
        }
        while (pA != null) {
            if (pA == pB) {
                return pA;
            }
            pA = pA.next;
            pB = pB.next;
        }
        return null;
    }
```

#### [53. 最大子序和](https://leetcode-cn.com/problems/maximum-subarray/)

```java
    public int maxSubArray(int[] nums) {
        int len = nums.length;
        int[] dp = new int[len];
        dp[0] = nums[0];
        int max = nums[0];
        for (int i = 1; i < len; i++) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            max = Math.max(dp[i], max);
        }
        return max;
    }
```

#### [155. 最小栈](https://leetcode-cn.com/problems/min-stack/)

```java
	Stack<Integer> realStack = null;
    Stack<Integer> minStack = null;
    private int min = Integer.MAX_VALUE;

    public MinStack() {
        realStack = new Stack<>();
        minStack = new Stack<>();
    }

    public void push(int x) {
        realStack.push(x);
        if (minStack.isEmpty()) {
            minStack.push(x);
        } else {
            if (minStack.peek() < x) {
                minStack.push(minStack.peek());
            } else {
                minStack.push(x);
            }
        }

    }
```

#### [1. 两数之和](https://leetcode-cn.com/problems/two-sum/)

```java
这个题没有难度,重点:
1.你可以假设每种输入只会对应一个答案。但是，你不能重复利用这个数组中同样的元素。  只对应一个答案的意思是,只有一对满足要求.
2.如果[3,3] target =6; 判断第二个3跟第一个不同的办法是  map.containsKey(two) && map.get(two) != i
public int[] twoSum(int[] nums, int target) {
        if (nums == null || nums.length <= 1) {
            return new int[]{};
        }
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i], i);
        }
        for (int i = 0; i < nums.length; i++) {
            int two = target - nums[i];

            if (map.containsKey(two) && map.get(two) != i) {
                int twoIndex = map.get(two);
                return new int[]{i, twoIndex};
            }
        }
        return new int[]{};
    }

```

#### [141. 环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)

```java
注意 如果考到这个题,注意看一下需不需要定位环形入口的位置.
    如果需要  则 x+z+y+z  = 2(x+z) ->  y = x
    如果不需要,则用下面的快慢指针即可.

快慢指针法
public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null)
            return false;
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && slow != null) {
            fast = fast.next;
            if (fast == slow)
                return true;
            if (fast != null) {
                fast = fast.next;
            } else {
                return false;
            }
            if (fast == slow)
                return true;
            slow = slow.next;
        }
        return false;
    }

哈希map记录是否访问过

    private HashSet<ListNode> set = new HashSet<ListNode>();

    public boolean hasCycle(ListNode head) {
        while (head != null) {
            if (set.contains(head)) {
                return true;
            } else {
                set.add(head);
            }
            head = head.next;
        }
        return false;
    }

```

#### @[234. 回文链表](https://leetcode-cn.com/problems/palindrome-linked-list/)

@还不熟练,容易出错.

```java
改进:

快慢指针分两种情况
1 2 3 4 5 奇数个: 结束的标志是    fast fast.next=null;fast在5处 slow为3是唯一中点,slow的next为下一半的开始
1 2 3 4	   偶数个: 结束的标志是   fast  fast =null  slow在3处是唯二的靠右边中点,slow即为下一半的开始
public boolean isPalindrome(ListNode head) {
       /* if (head == null) {
            return true;
        }
        if (head.next == null) {
            return true;
        }
        if (head.next.next == null) {
            return head.val == head.next.val;
        }*/
        ListNode slow = head;
        ListNode pre = head;
        ListNode fast = head;
        ListNode leftHead = new ListNode(0);
        while (fast != null && fast.next != null) {
            pre = slow;
            slow = slow.next;

            fast = fast.next.next;

            pre.next = leftHead.next;
            leftHead.next = pre;

        }
        ListNode left = leftHead.next;
        ListNode right = null;
        if (fast == null) {
            right = slow;
        } else {
            right = slow.next;
        }

        while (left != null && right != null) {
            if (left.val != right.val) {
                return false;
            }
            left = left.next;
            right = right.next;
        }
        return true;
    }
```

#### [@20. 有效的括号](https://leetcode-cn.com/problems/valid-parentheses/)

```java

Stack<Character> stack = new Stack<>();
    HashMap<Character, Character> map = new HashMap<>();

    public boolean isValid(String s) {
        map.put(')', '(');
        map.put('}', '{');
        map.put(']', '[');
        if (s == null || s.length() == 0) {
            return true;
        }
        char[] chars = s.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            if (stack.isEmpty()) {
                stack.push(chars[i]);
            } else {
                if (map.get(chars[i]) == stack.peek()) {
                    stack.pop();
                } else {
                    stack.push(chars[i]);
                }
            }
        }
        return stack.isEmpty();
    }
```

#### @[39. 组合总和](https://leetcode-cn.com/problems/combination-sum/)

```java
比较规范的回溯写法

    backtracing(path,begin,nums,sum){
    	//终结条件
    if(begin<len){

        }
    	if(sum==target){

        }
    	if(sum>tartget){

        }
    //各种可能
    //其中,传值类的直接加在形参上,如begin+1;地址类的 夹住backtracing,用add()和remove();
    	for(...){
            list.add()
            backtracing(list,begin+1,nums,sum+nums[i]);
            list.remove()
        }
}

List<List<Integer>> ret = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0) {
            return ret;
        }
        backtracing(candidates, target, 0, 0, path);
        return ret;
    }

    private void backtracing(int[] candidates, int target, int begin, int sum, List<Integer> path) {
        if (begin > candidates.length - 1) {
            return;
        }
        if (sum > target) {
            return;
        }
        if (sum == target) {
            List<Integer> temp = new ArrayList<>(path);
            ret.add(temp);
            return;
        }
        for (int i = begin; i < candidates.length; i++) {
            path.add(candidates[i]);
            backtracing(candidates, target, i, sum + candidates[i], path);
            path.remove(path.size() - 1);
        }
    }

	private List<List<Integer>> ret = new ArrayList<>();
    private List<Integer> list = new ArrayList<>();
    int target = 0;
    int len = 0;

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0 || target < 0) {
            return ret;
        }
        len = candidates.length;
        this.target = target;
        backtracing(0, 0, candidates);
        return ret;
    }

    private void backtracing(int begin, int sum, int[] candidates) {
        if (sum == target) {
            ret.add(new ArrayList(list));
        } else if (sum > target) {
            return;
        }
        for (int i = begin; i < len; i++) {
            list.add(candidates[i]);
            backtracing(i, sum + candidates[i], candidates);
            list.remove(list.size() - 1);
        }
    }
```

#### [22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/)

```java
改进:
String的回溯用String的拼接就比较好  temp+"sdf"; 因为String的删除比较麻烦,而StringBuilder的删除也很耗时,如果题目要求返回的是String而不是StringBuilder,那么不要用StringBuilder来做转化.

 List<String> ret = new ArrayList<>();
    char[] path;
    int leftAll = 0;
    int leftCount = 0;
    int rightCount = 0;
    int all = 0;

    public List<String> generateParenthesis(int n) {
        if (n == 0) {
            return ret;
        }
        leftAll = n;
        all = 2 * n;
        path = new char[all];
        backtracing(0, path, 0, 0);
        return ret;
    }

    private void backtracing(int begin, char[] path, int leftCount, int rightCount) {
        if (rightCount > leftCount || leftCount > leftAll || begin > all) {
            return;
        }
        if (begin == path.length && leftCount == leftAll) {
            String temp = new String(path);
            ret.add(temp);
            return;
        }

        path[begin] = '(';
        backtracing(begin + 1, path, leftCount + 1, rightCount);

        path[begin] = ')';
        backtracing(begin + 1, path, leftCount, rightCount + 1);

    }
```

#### [105. 从前序与中序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)

```java
 public TreeNode buildTree(int[] preorder, int[] inorder) {
        return buildTree(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
    }

    private TreeNode buildTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
        if (preStart > preEnd || inStart > inEnd || preStart < 0 || preEnd >= preorder.length || inStart < 0 || inEnd >= inorder.length) {
            return null;
        }
        int rootVal = preorder[preStart];
        int cutIndex = 0;
        for (int i = inStart; i <= inEnd; i++) {
            if (inorder[i] == rootVal) {
                cutIndex = i;
                break;
            }
        }
        int leftLen = cutIndex - inStart;
        TreeNode root = new TreeNode(rootVal);
        root.left = buildTree(preorder, preStart + 1, preStart + leftLen, inorder, inStart, cutIndex - 1);
        root.right = buildTree(preorder, preStart + leftLen + 1, preEnd, inorder, cutIndex + 1, inEnd);
        return root;
    }
```

#### [102. 二叉树的层次遍历](https://leetcode-cn.com/problems/binary-tree-level-order-traversal/)

```java
改进:
思路有 1. 层次遍历(每层入队列的时候记录下一层有多少个子节点)for (int i = 0; i < levelCount; i++)
      2.深度优先遍历(先根遍历)

//层次遍历源码
Queue<TreeNode> queue = new LinkedList<TreeNode>();

    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<>();
        if (root == null) {
            return ret;
        }
        queue.add(root);
        int levelCount = 1;

        while (!queue.isEmpty()) {
            List<Integer> list = new ArrayList<Integer>();
            int temp = 0;
            for (int i = 0; i < levelCount; i++) {
                TreeNode node = queue.poll();
                list.add(node.val);
                if (node.left != null) {
                    queue.add(node.left);
                    temp++;
                }
                if (node.right != null) {
                    queue.add(node.right);
                    temp++;
                }
            }
            levelCount = temp;
            ret.add(list);
        }
        return ret;
    }

//深度优先源码

List<List<Integer>> ret = new ArrayList<>();

    public List<List<Integer>> levelOrder(TreeNode root) {
        if (root == null) {
            return ret;
        }
        preOrder(root, 0);

        return ret;

    }

    private void preOrder(TreeNode root, int level) {
        if (root == null) {
            return;
        }
        List<Integer> temp = null;
        if (ret.size() <= level) {
            temp = new ArrayList<Integer>();
            ret.add(temp);
        }
        temp = ret.get(level);
        temp.add(root.val);
        preOrder(root.left, level + 1);
        preOrder(root.right, level + 1);
    }
```

#### [347. 前 K 个高频元素](https://leetcode-cn.com/problems/top-k-frequent-elements/)

@基本方法已经掌握了,注意他这里的一个技巧. 他的优选队列里面存的是num而非frequency. num的比较用的是

Comparator<Integer> c = new Comparator<Integer>() {
            public int compare(Integer i1, Integer i2) {
                return map.get(i1) - map.get(i2);
            }
        };

内部的比价器居然可以用外部的map.

```java
改进:
map来记录出现次数
小顶堆来记录topk

注意比较器的写法:return map.get(i1) - map.get(i2);
注意k不能决定堆得最大容量,堆会自动扩容,所以要在add元素进堆得时候进行大小判断,如果size()>k,则add之后poll一个出来

    HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
    public List<Integer> topKFrequent(int[] nums, int k) {
        Comparator<Integer> c = new Comparator<Integer>() {
            public int compare(Integer i1, Integer i2) {
                return map.get(i1) - map.get(i2);
            }
        };
        PriorityQueue<Integer> heap = new PriorityQueue<Integer>(k, c);
        for (int temp : nums) {
            if (map.get(temp) == null) {
                map.put(temp, 1);
            } else {
                map.put(temp, map.get(temp) + 1);
            }
        }
        Set<Map.Entry<Integer, Integer>> s = map.entrySet();
        Iterator<Map.Entry<Integer, Integer>> i = s.iterator();
        while (i.hasNext()) {
            if (heap.size() < k) {
                heap.add(i.next().getKey());
            } else {
                heap.add(i.next().getKey());
                heap.poll();
            }
        }
        List<Integer> list = new ArrayList<Integer>();
        while (!heap.isEmpty()) {
            list.add(heap.poll());
        }
        return list;
    }
```

#### [17. 电话号码的字母组合](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/)

```java
回溯,没啥好说的

Map<String, String> numsAndLetters = new HashMap<String, String>() {{
    put("2", "abc");
    put("3", "def");
    put("4", "ghi");
    put("5", "jkl");
    put("6", "mno");
    put("7", "pqrs");
    put("8", "tuv");
    put("9", "wxyz");
}};
List<String> ret = new ArrayList<String>();
int len = 0;
public List<String> letterCombinations(String digits) {
    if (digits == null || digits.length() == 0) {
        return ret;
    }
    len = digits.length();
    backtracing(0, "", digits);
    return ret;
}

private void backtracing(Integer index, String path, String digits) {
    if (index == len) {
        ret.add(path);
        return;
    }
    String numLetters = numsAndLetters.get(digits.substring(index, index + 1));
    for (int i = 0; i < numLetters.length(); i++) {
        String temp = numLetters.substring(i, i + 1);
        backtracing(index + 1, path + temp, digits);
    }
}
```

#### [4. 寻找两个有序数组的中位数](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/)

```java
思路1: 把两个数组合并,然后寻找中位数.

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    if (nums1 == null || nums2 == null) {
        return 0;
    }
    int m = nums1.length;
    int n = nums2.length;
    int[] allNums = new int[m + n];
    int count = 0;
    int i = 0;
    int j = 0;
    while (i < m && j < n && count < m + n) {
        if (nums1[i] < nums2[j]) {
            allNums[count++] = nums1[i++];
        } else {
            allNums[count++] = nums2[j++];
        }
    }
    while (i < m) {
        allNums[count++] = nums1[i++];
    }
    while (j < n) {
        allNums[count++] = nums2[j++];
    }
    if (count % 2 == 0) {
        return ((double) allNums[count / 2] + (double) allNums[count / 2 - 1]) / 2;
    } else {
        return allNums[count / 2];
    }
}
```

#### 要再刷一遍

#### [46. 全排列](https://leetcode-cn.com/problems/permutations/)

```java
改进  典型的回溯法问题,一定要熟练掌握.

private List<List<Integer>> ret = new ArrayList<>();
boolean[] visited;
private List<Integer> path = new ArrayList<>();

public List<List<Integer>> permute(int[] nums) {
    int len = nums.length;
    visited = new boolean[len];
    backtracing(nums);

    return ret;
}

private void backtracing(int[] nums) {
    if (path.size() == nums.length) {
        ret.add(new ArrayList(path));
    }
    for (int i = 0; i < nums.length; i++) {
        if (visited[i])
            continue;

        path.add(nums[i]);
        visited[i] = true;
        backtracing(nums);
        visited[i] = false;
        path.remove(path.size() - 1);
    }

}
```

#### [78. 子集](https://leetcode-cn.com/problems/subsets/)

```java
   List<List<Integer>> ret = new ArrayList<>();
    ArrayList<Integer> list = new ArrayList<>();
    int len = 0;

    public List<List<Integer>> subsets(int[] nums) {
        len = nums.length;
        backtracing(0, nums);
        return ret;
    }

    private void backtracing(int begin, int[] nums) {
        ret.add(new ArrayList(list));
        for (int i = begin; i < len; i++) {
            list.add(nums[i]);public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || nums.length < k) {
            return new int[]{};
        }
        Comparator<Integer> c = new Comparator<Integer>() {
            public int compare(Integer i1, Integer i2) {
                return nums[i2] - nums[i1];
            }
        };
        ArrayList<Integer> ret = new ArrayList<>();
        PriorityQueue<Integer> heap = new PriorityQueue<Integer>(10, c);
        int right = 0;
        int len = nums.length;
        for (right = 0; right <= len; right++) {
            if (right < k) {
                heap.add(right);
                continue;
            }
            ret.add(nums[heap.peek()]);
            heap.remove(right - k);
            if (right < len) {
                heap.add(right);
            }
        }
        int[] retArray = new int[ret.size()];
        for (int i = 0; i < ret.size(); i++) {
            retArray[i] = ret.get(i);
        }
        return retArray;
    }
            backtracing(i + 1,nums);
            list.remove(list.size()-1);
        }
    }
```

#### [@338. 比特位计数](https://leetcode-cn.com/problems/counting-bits/)

@思路没想到

```java
改进 动态规划的方法
    0 可以得出 1               2^0
    0 和 1 能得出  2 和 3       2^1
    0 1 2 3 能得出  4 5 6 7    2^2
public int[] countBits(int num) {
        int[] ans = new int[num + 1];
        ans[0] = 0;
        int b = 1;
        int i = 0;
        while (b <= num) {
            while (i < b && b + i <= num) {
                ans[b + i] = ans[i] + 1;
                i++;
            }
            b <<= 1;

            i = 0;
        }
        return ans;
    }
```

#### 543. [二叉树的直径](https://leetcode-cn.com/problems/diameter-of-binary-tree/)

```java
改进: 先根遍历/也是深度优先搜索 ,每个root求左右子树长度之和,注意最终的直径也许不会经过根节点,比如
      2
    3   1
  6  4
  9
  8
private int maxLength = Integer.MIN_VALUE;

    public int diameterOfBinaryTree(TreeNode root) {
        if (root == null)
            return 0;
        preOrder(root);
        return maxLength;
    }

    private int preOrder(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int lenL = preOrder(root.left);
        int lenR = preOrder(root.right);
        int length = lenL + lenR; //root不算在总长度里面
        maxLength = Math.max(maxLength, length);
        return Math.max(lenL, lenR) + 1;
    }
```

#### @[96. 不同的二叉搜索树](https://leetcode-cn.com/problems/unique-binary-search-trees/)

```java
改进: 从1~n,每个节点都可以当根来组成二叉搜索树.
    如果1是根,则左右两边的节点数分别是0,n-1;能组成的搜索树个数为:dp[0]*dp[n-1]
    如果2是根,则左右两边的节点数分别是1,n-2;能组成的搜索树个数为:dp[1]*dp[n-2]
    所以第一个循环是从i=2求到i=n的dp[n]有多少种搜索树的组成
    第二个循环是在求dp[i]的1~分别当根节点,组成的搜索树个数

public int numTrees(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
```

#### [64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/)

```java
	如果是求和,用动规. 如果求path的具体内容比如匹配路径字符串(abcd)则用回溯

改进:用回溯法会超时;不过注意回溯法的用法:
	不要用xy   x和y在数组的申请和定位数据的时候是反直觉的grid[y][x] new int[yBound][xBound]
    用row col  row和col的含义是第几row和 第几col
    grid[row][col] new int [row][col]

    boolean[][] visited = null;
    int min = Integer.MAX_VALUE;
    int[][] loc = new int[][]{{1, 0}, {-1, 0}, {0, -1}, {0, 1}};
    int path = 0;
    int targetRow = 0;
    int targetCol = 0;

    public int minPathSum(int[][] grid) {
        int rowCount = grid.length;
        int colCount = grid[0].length;
        targetRow = rowCount - 1;
        targetCol = colCount - 1;
        visited = new boolean[rowCount][colCount];
        visited[0][0] = true;
        path = grid[0][0];
        backtracing(grid, path, 0, 0);
        return min;
    }

    private void backtracing(int[][] grid, int path, int row, int col) {

        if (row == targetRow && col == targetCol) {
            min = Math.min(min, path);
        }
        for (int i = 0; i < loc.length; i++) {
            col = col + loc[i][1];
            row = row + loc[i][0];
            if (!(row < 0 || col < 0 || row > targetRow || col > targetCol || visited[row][col] == true)) {
                visited[row][col] = true;
                backtracing(grid, path + grid[row][col], row, col);
                visited[row][col] = false;
            }
            col = col - loc[i][1];
            row = row - loc[i][0];
        }
    }

改进:动态规划的做法
    状态转移方程: dp(i,j)=grid(i,j)+min(dp(i+1,j),dp(i,j+1))

    时间复杂度O(mn)已经无法优化,空间复杂度O(mn)可以优化为O(1),就是用grid本身来做dp.

    public int minPathSum(int[][] grid) {
        int rowCount = grid.length;
        int colCount = grid[0].length;
        int[][] dp = new int[rowCount][colCount];
        dp[0][0] = grid[0][0];
        for (int i = 1; i < rowCount; i++) {
            dp[i][0] = grid[i][0] + dp[i - 1][0];
        }
        for (int j = 1; j < colCount; j++) {
            dp[0][j] = grid[0][j] + dp[0][j - 1];
        }
        for (int i = 1; i < rowCount; i++) {
            for (int j = 1; j < colCount; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        return dp[rowCount - 1][colCount - 1];
    }
```

#### [437. 路径总和 III](https://leetcode-cn.com/problems/path-sum-iii/)

```java
改进:双递归的题,有点难度.
    pathSum的递归用于遍历所有节点,根节点是不断变化的
    pathFromRoot的递归用于找到一条root为根节点,root的子节点为终止节点的路径

public int pathSum(TreeNode root, int sum) {
        if (root == null) {
            return 0;
        }
        return pathFromRoot(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
    }

    private int pathFromRoot(TreeNode root, int sum) {
        if (root == null) {
            return 0;
        }
        int ret = 0;
        if (root.val == sum) {
            ret = 1;
        }
        ret += pathFromRoot(root.left, sum - root.val);
        ret += pathFromRoot(root.right, sum - root.val);
        return ret;
    }
```

#### [238. 除自身以外数组的乘积](https://leetcode-cn.com/problems/product-of-array-except-self/)

```java
改进:空间复杂度O(1) 要么是在ret上做累加,要么是原数组nums上做修改
	时间复杂度O(n)必然是之前计算的结果可以拿到后面用.
	ret[i] 等于nums的 i左边的数 乘以 i右边的数
public int[] productExceptSelf(int[] nums) {
        int len = nums.length;
        int[] ret = new int[len];
        ret[0] = 1;
        for (int i = 1; i < len; i++) {
            ret[i] = ret[i - 1] * nums[i - 1];
        }
        int temp = 1;
        for (int i = len - 1; i >= 0; i--) {
            ret[i] = ret[i] * temp;
            temp = temp * nums[i];
        }
        return ret;
    }
```

#### [48. 旋转图像](https://leetcode-cn.com/problems/rotate-image/)

```java
改进: 先转置,在镜像翻转  =  往右旋转九十度

public void rotate(int[][] matrix) {
        int len = matrix.length;
        //转置
        for (int i = 0; i < len; i++) {
            for (int j = i + 1; j < len; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }
        //镜像翻转
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len / 2; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[i][len - j - 1];
                matrix[i][len - j - 1] = temp;
            }
        }
    }
```

#### [49. 字母异位词分组](https://leetcode-cn.com/problems/group-anagrams/)

方法1:下面代码:

方法2: a 对应0  b对应1  c对应2.....

int[] prime = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103 };

例如 abc ，就对应 'a' - 'a'， 'b' - 'a'， 'c' - 'a'，即 0, 1, 2，也就是对应素数 2 3 5，然后相乘 2 * 3 * 5 = 30，就把 "abc" 映射到了 30。

方法3:

首先初始化 key = "0#0#0#0#0#"，数字分别代表 abcde 出现的次数，# 用来分割。

这样的话，"abb" 就映射到了 "1#2#0#0#0"。

"cdc" 就映射到了 "0#0#2#1#0"。

"dcc" 就映射到了 "0#0#2#1#0"。

1. 用一个cout[]数组记录每个字符串中每个字母出现的次数
2. key用StringBuilder来维护,把一个count[]数组append进key里面
3. 把key作为map的key

```java
改进:
每个字符串"cba" 用排序之后的"abc"作为key存储在HashMap<String, ArrayList<String>>;
key相同的字符串放在map的value即ArrayList里面

public List<List<String>> groupAnagrams(String[] strs) {
        HashMap<String, ArrayList<String>> map = new HashMap<String, ArrayList<String>>();
        for (int i = 0; i < strs.length; i++) {
            String str = strs[i];
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            String strSort = new String(chars);

            if (map.get(strSort) == null) {
                ArrayList<String> list = new ArrayList<String>();
                list.add(str);
                map.put(strSort, list);
            } else {
                ArrayList<String> list = map.get(strSort);
                list.add(str);
                map.put(strSort, list);
            }
        }
        List<List<String>> ret = new ArrayList<>();
        Set<Map.Entry<String, ArrayList<String>>> s = map.entrySet();
        Iterator<Map.Entry<String, ArrayList<String>>> it = s.iterator();
        while (it.hasNext()) {
            ret.add(it.next().getValue());
        }
        return ret;
    }

```

#### [@739. 每日温度](https://leetcode-cn.com/problems/daily-temperatures/)

@此题栈的使用太巧秒了

栈里面存的是升序的,比T[i]处温度大的一些温度.

```java
改进:逆序遍历,记录以往的温度的最大值,因为如果 倒数第二天温度是75, 最后一天温度是70,则倒数第三天的时候如果温度是76比75大,则继续往后找也找不到比76大的天气了,因为75是未来几天最大的温度.

public int[] dailyTemperatures(int[] T) {
        Stack<Integer> stack = new Stack<>();
        int len = T.length;
        int[] ret = new int[len];
        for (int i = len - 1; i >= 0; i--) {
            int temp = T[i];
            while (!stack.isEmpty()) {
                if (temp >= T[stack.peek()]) {
                    stack.pop();
                } else {
                    ret[i] = stack.peek() - i;
                    break;
                }
            }
            stack.push(i);
        }
        return ret;
 }

改进:获取比当前温度temp高的所有温度的日期,找距离当前日期最近的那个

public int[] dailyTemperatures(int[] T) {
        int len = T.length;
        int[] ans = new int[len];
        int[] next = new int[101];
        for (int i = len - 1; i >= 0; i--) {
            int temp = T[i];
            next[temp] = i;
            int warmerIndex = Integer.MAX_VALUE;
            ans[i] = 0;
            for (int j = temp + 1; j <= 100; j++) {
                if (next[j] != 0) {
                    warmerIndex = Math.min(next[j], warmerIndex);
                    ans[i] = warmerIndex - i;
                }
            }

        }
        return ans;
    }
```

#### [@406. 根据身高重建队列](https://leetcode-cn.com/problems/queue-reconstruction-by-height/)

```java
改进:先根据身高从大到小,k从小到大来排数组,然后将排好序的数组往另一个数组里插入,插完就是想要的结果
public int[][] reconstructQueue(int[][] people) {
    Comparator c = new Comparator<int[]>() {
        public int compare(int[] o1, int[] o2) {
            return o1[0] == o2[0] ? o1[1] - o2[1] : o2[0] - o1[0];
        }
    };
    Arrays.sort(people, c);
    ArrayList<int[]> list = new ArrayList<>();
    for (int[] temp : people) {
        list.add(temp[1], temp);
    }
    return list.toArray(new int[list.size()][2]);
}
```

#### [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/)

```java
改进:动态规划类的题目,找好状态转义方程+定好不满足方程的边界条件就可以解出来

dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); 第i天的收获 = Max(昨天,前天+今天);
关于边界处的思考: dp[0] = nums[0];dp[1] = Math.max(nums[0], nums[1]);也就是说dp[1]可以是nums[0]也可以是nums[1],我会有疑惑: 如果dp[1]选的是nums[0],那dp[2]在选dp[1]=nums[0]的时候还有必要隔一天吗?直接dp[2]=dp[1]+nums[2]不就好了

事实上,dp[1] =  nums[0]  或  nums[1]
     dp[2] =  max(dp[1],dp[0]+nums[2])分下面两种情况
    当dp[1] = nums[1]时
		max(dp[1],dp[0]+nums[2])其实是  nums[1] 和 nums[0]+nums[2]比较大小
     当dp[1] = nums[0]时
    	 max(dp[1],dp[0]+nums[2])其实是  nums[0] 和 nums[0]+nums[2]比较大小;该情况下必选dp[0]+nums[2],所以上面疑惑的那种情况出现的时候,选dp[1]+nums[2](也就是dp[0]+nums[2])而非dp[1]是必然的,而且能满足隔一个房间.

public int rob(int[] nums) {
    int len = nums.length;
    if (len == 0)
        return 0;
    if (len == 1) {
        return nums[0];
    }
    int[] dp = new int[len];
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);
    for (int i = 2; i < len; i++) {
        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
    }
    return dp[len - 1];
}
```

#### [@279. 完全平方数](https://leetcode-cn.com/problems/perfect-squares/)

一维的,从小到大,但是不相邻的动态规划

@思考 什么样的问题用动规解决比较好 (从小到大的累计过程用动规比较好)

本题的动规方程为啥没想到

```java
改进:状态转移方程 dp[i] = Math.min(dp[i],dp[i-1],dp[i-4],dp[i-16],dp[i-j*j])

public int numSquares(int n) {
    int[] dp = new int[n + 1];
    for (int i = 1; i <= n; i++) {
        dp[i] = i;
        for (int j = 1; i - j * j >= 0; j++) {
            dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
        }
    }
    return dp[n];
}

```

#### [75. 颜色分类](https://leetcode-cn.com/problems/sort-colors/)

```java
改进:统计每个颜色有几个,然后填到数组中

//借助常数空间意味着在原来的数组上改动,并且用temp之类的变量来记录信息
    public void sortColors(int[] nums) {
        HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
        for (int temp : nums) {
            if (map.get(temp) == null) {
                map.put(temp, 1);
            } else {
                map.put(temp, map.get(temp) + 1);
            }
        }
        int color = -1;
        int colorCount = 0;
        for (int i = 0; i < nums.length; ) {
            if (colorCount > 0) {
                nums[i++] = color;
                colorCount--;
            } else {
                color++;
                if (map.get(color) == null) {
                    colorCount = 0;
                } else {
                    colorCount = map.get(color);
                }
            }
        }
    }

改进: 思路  left->   cur->   <-right
 //常数空间(用一些小数据记录信息,temp交换数据))+一趟遍历(修改原数组)
    public void sortColors(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        int cur = 0;
        while (cur <= right) {
            if (nums[cur] == 0) {
                int temp = nums[cur];
                nums[cur] = nums[left];
                nums[left] = temp;
                left++;
                cur++;//left++的时候cur++是因为cur是从左边开始往右走,
                //如果left++,代表cur的左边界往右走去了.这个时候cur++就又回到了左边界最开始的地方
            } else if (nums[cur] == 2) {
                int temp = nums[cur];
                nums[cur] = nums[right];
                nums[right] = temp;
                right--;
            } else {
                cur++;
            }
        }
    }
```

#### [142. 环形链表 II](https://leetcode-cn.com/problems/linked-list-cycle-ii/)

```java
改进:公式  x + y+ z + y = 2(x+y)推出  x = z 所以这个时候把快指针放到phead处,走x;慢指针走z;两个指针就会在
    环入口处相遇

    注意代码的逻辑上有几个要注意的问题
     while (fast != null && fast.next != null) {//走两步必备的判空

     if (fast == null || fast.next == null) {  //如果fast是因为空指针跳出while循环则不存在环
        return null;
    }

public ListNode detectCycle(ListNode head) {
    if (head == null) {
        return null;
    }
    if (head.next == null) {
        return null;
    }
    ListNode fast = head;
    ListNode slow = head;
    while (fast != null && fast.next != null) {//走两步必备的判空
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow) {
            break;
        }
    }
    if (fast == null || fast.next == null) {  //如果fast是因为空指针跳出while循环则不存在环
        return null;
    }
    fast = head;
    while (fast != slow) {                  //如果fast不是因为空指针跳出while循环,fast和slow分别在
        									//走了x 和 z步后相遇(x=z)
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}
```

#### [@@394. 字符串解码](https://leetcode-cn.com/problems/decode-string/)

@@//字母 数字  [  ] 四种情况   +

  stringbuilder的应用 +

栈的应用,括号类问题的处理

```java
改进:代码太牛,记住就好
public String decodeString(String s) {
        StringBuilder curStr = new StringBuilder();
        int curCount = 0;
        Stack<StringBuilder> stack_str = new Stack<StringBuilder>();
        Stack<Integer> stack_num = new Stack<Integer>();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == '[') {
                stack_str.push(curStr);
                stack_num.push(curCount);
                curStr = new StringBuilder();
                curCount = 0;
            } else if (ch == ']') {
                StringBuilder temp = new StringBuilder();
                int count = stack_num.pop();
                for (int j = 0; j < count; j++) {
                    temp.append(curStr.toString());
                }
                curStr = stack_str.pop();
                curStr.append(temp);
            } else if (ch >= '0' && ch <= '9') {
                curCount = curCount * 10 + ch - '0';
            } else if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
                curStr.append(ch);
            }
        }
        return curStr.toString();
    }
```

#### @@@@@动态规划类题目的反思

1. 明确末尾return的是boolean还是 int 这决定了dp里面存的是啥
2. 然后确定dp是二维还是一维.最终状态由多少个维度决定
    1. 比如目标和类的题目.因为`dp[i][S]中S`可能由多种前状态得出,所以需要第二维来记录值.
    2. 比如最大子序列,只需要len一个状态
    3. 交易次数的股票: i天数  k 交易次数  0 1 是否持有股票
3. 然后明确状态转移方程
    1. 如果是必须要加减nums[i].则
        `dp[i][j] = dp[i-1][j-nums[i]] || dp[i-1][j+nums[i]];`
    2. 如果是可以抛弃当前nums[i]
        `dp[i][j] = dp[i-1][j]||dp[i-1][j-nums[i]];`
    3. 如果是涉及最大值问题(比如最大上升子序列)
        dp[i]代表到i的时候,最长序列是啥
        dp[i] = Math.max(dp[i],dp[j]+1)
4. **同时注意状态转移的条件和各种状态的初始化**比如最大上升子序列:初始化dp[i]= 1 和  nums[i]>=nums[j]的条件.比如兑换硬币类的题目,dp[i]最多可以被1兑换成i个,所以dp[i]初始化为i.

#### [@@337. 打家劫舍 III](https://leetcode-cn.com/problems/house-robber-iii/)

改进:树的后续遍历+动态规划

```java

HashMap<TreeNode, Integer> memo = new HashMap<TreeNode, Integer>();

public int rob(TreeNode root) {
    if (root == null) {
        return 0;
    }
    lastOrder(root);
    return memo.get(root);
}

private int lastOrder(TreeNode root) {
    if (root == null) {
        return 0;
    }

    if (memo.containsKey(root)) return memo.get(root);
    int money1 = root.val;
    int money2 = 0;
    int maxMoney = 0;
    if (root.left != null) {
        money2 += lastOrder(root.left);
        money1 += lastOrder(root.left.left);
        money1 += lastOrder(root.left.right);

    }
    if (root.right != null) {
        money2 += lastOrder(root.right);
        money1 += lastOrder(root.right.left);
        money1 += lastOrder(root.right.right);

    }
    maxMoney = Math.max(money1, money2);
    memo.put(root, maxMoney);
    return maxMoney;
}
```

#### [@@121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)

@/0-prices[i]而非dp[i-1][1]-prices[i]是为了防止多次买卖,

```java
 public int maxProfit(int[] prices) {
        if (prices == null || prices.length <= 1) {
            return 0;
        }
        int len = prices.length;
        int[][] dp = new int[len + 1][2];
        dp[1][0] = 0;
        dp[1][1] = -prices[0];
        for (int i = 2; i <= len; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
              //0-prices[i]而非dp[i-1][1]-prices[i]是为了防止多次买卖,
            dp[i][1] = Math.max(dp[i - 1][1], -prices[i - 1]);
        }
        return dp[len][0];
    }

```

#### [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/)

```java
public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        int len = prices.length;
        int[][] dp = new int[len][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < len; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[len - 1][0];
    }
```

#### [@@309. 最佳买卖股票时机含冷冻期](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/)

改进:动态规划类的题目:1.找状态和选择2.定好base_case 就这两点而已,详情看下面大神题解
    https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-lab/

```java
改进后:
public int maxProfit(int[] prices) {
        if (prices == null || prices.length <= 1) {
            return 0;
        }
        int len = prices.length;
        int[][] dp = new int[len + 1][2];
        dp[1][0] = 0;
        dp[1][1] = -prices[0];

        for (int i = 2; i <= len; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            //i-2其实这里要对i=2 时候  dp[0][0]的边界关注一下,好处是dp[0][0]=0
            //依然符合状态转移方程
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1]);
        }
        return dp[len][0];
    }

改进前:

 public int maxProfit(int[] prices) {
        if (prices == null) {
            return 0;
        }
        int len = prices.length;

        if (len <= 1) {
            return 0;
        }
        int[][] dp = new int[len + 1][2];
     	//第一天初始化
        dp[1][0] = 0;
        dp[1][1] = -prices[0];
		//第二天初始化,因为第二天不能用  dp[i - 2][0] - prices[i - 1]   (其实也可以用,因为用dp[0][0]=0)
     	dp[2][0] = Math.max(dp[1][0], dp[1][1] + prices[1]);
        dp[2][1] = Math.max(dp[1][1], dp[1][0] - prices[1]);
     	//从第三天开始可以用状态转移方程
        for (int i = 3; i <= len; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1]);
        }
        return dp[len][0];
    }
```

#### [@@123. 买卖股票的最佳时机 III](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/)

```java
@@@
1.关于i=0时的边界条件
dp[0][k][0] = 0;
dp[0][k][1] = -prices[0];
无论k为0,1,2这个赋值都成立.
2.列出状态转移方程:
k代表的是剩余交易次数.每次卖出股票后,算消耗了一次交易次数.
if (k - 1 >= 0) {
     dp[i][k - 1][0] = Math.max(dp[i - 1][k - 1][0], dp[i - 1][k][1] + prices[i]);
 }
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k][0] - prices[i]);

```

```java
public int maxProfit(int[] prices) {
        if (prices == null || prices.length <= 1) {
            return 0;
        }
        int len = prices.length;
        int[][][] dp = new int[len + 1][3][2];
        dp[1][2][1] = Integer.MIN_VALUE;
        dp[1][2][0] = 0;
        dp[1][1][1] = -prices[0];
        dp[1][1][0] = 0;
        dp[1][0][1] = -prices[0];
        dp[1][0][0] = 0;
        //卖出时记录消耗一笔
        for (int i = 2; i <= len; i++) {
            for (int k = 0; k <= 2; k++) {
                //dp[i][k][1]里面的k取值为 0 1   2是不行的,所以设为minvalue
                //因为dp[i][k][0]会用到dp[i][2][1].所以先设置dp[i][2][1];在计算dp[i][k][0]
                if (k == 2) {
                    dp[i][2][1] = Integer.MIN_VALUE;
                } else if (k <= 1) {
                    dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k + 1][0] - prices[i - 1]);
                }
                //dp[i][k][0]里面的k取值为 0 1 2
                dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i - 1]);
            }
        }
        return dp[len][0][0];
    }
```

#### [@621. 任务调度器](https://leetcode-cn.com/problems/task-scheduler/)

@优先队列,来确定执行顺序

@每个n为一个轮回,既可以解决空转,又可以让数量多的任务先运行

```java
改进:
容易出错的地方:
1.当queue是空并且list里面也没有候选,要提前退出,cpu不需要走完完整的一个轮回(n+1)个任务
    if (frequencyOfChar.isEmpty() && frequencyDcreByOneList.size() == 0)
                    break;

public int leastInterval(char[] tasks, int n) {
        HashMap<Character, Integer> freqCount = new HashMap<>();
        PriorityQueue<Integer> freqQueue = new PriorityQueue<Integer>(10, (o1, o2) -> (o2 - o1));
        for (int i = 0; i < tasks.length; i++) {
            char ch = tasks[i];
            if (freqCount.containsKey(ch)) {
                freqCount.put(ch, freqCount.get(ch) + 1);
            } else {
                freqCount.put(ch, 1);
            }
        }
        Iterator<Map.Entry<Character, Integer>> it = freqCount.entrySet().iterator();
        while (it.hasNext()) {
            freqQueue.add(it.next().getValue());
        }
        int timeCount = 0;
        while (true) {
            List<Integer> ready = new ArrayList<Integer>();
            for (int i = 0; i <= n; i++) {
                if (!freqQueue.isEmpty()) {
                    int max = freqQueue.poll();
                    max--;
                    if (max >= 1) {
                        ready.add(max);
                    }
                }
                timeCount++;
                if (freqQueue.isEmpty() && ready.size() == 0) {
                    return timeCount;
                }
            }
            for (int j = 0; j < ready.size(); j++) {
                freqQueue.add(ready.get(j));
            }
        }
    }

```

#### [@@146. LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/)

@双向链表的做法.

```java
改进:LinkedHashMap 可以在构造的时候指定,是否按照访问顺序来存储节点(key)
    访问的时候,从链表中拿到key,在用key从map中拿到value

    思路1:
    class LRUCache {
        private LinkedHashMap<Integer, Integer> map;

        public LRUCache(int capacity) {
            map = new LinkedHashMap<Integer, Integer>(capacity, 0.75F, true) {
                public boolean removeEldestEntry(Map.Entry<Integer, Integer> entry) {
                    return size() > capacity;
                }
            };
        }

        public int get(int key) {
            return map.getOrDefault(key, -1);
        }

        public void put(int key, int value) {
            map.put(key, value);
        }
    }

思路2:  有个弊端,list.remove的时候复杂度是O(n)	另外注意list.remove((Object)key)  和 remove.(Index)
class LRUCache {
        LinkedList<Integer> list = null;
        HashMap<Integer, Integer> map = null;
        int cap = 0;

        public LRUCache(int capacity) {
            list = new LinkedList<>();
            map = new HashMap<>();
            this.cap = capacity;
        }

        public int get(int key) {
            Integer val = map.get(key);
            if (val != null) {
                list.remove((Object) key);
                list.addLast(key);
                return val;
            }
            return -1;
        }

        public void put(int key, int value) {
            Integer val = map.get(key);
            if (val != null) {
                list.remove((Object) key);
                list.addLast(key);
                map.put(key, value);
                return;
            }
            if (list.size() < cap) {
                list.addLast(key);
                map.put(key, value);
            } else {
                Integer firstKey = list.getFirst();
                map.remove(firstKey);
                list.removeFirst();
                map.put(key, value);
                list.addLast(key);
            }
        }
    }
```

#### [@@@416. 分割等和子集](https://leetcode-cn.com/problems/partition-equal-subset-sum/)

```java
改进:01背包问题
 public boolean canPartition(int[] nums) {
        if (nums == null || nums.length == 0) {
            return false;
        }
        int len = nums.length;
        int sum = 0;
        for (int temp : nums) {
            sum += temp;
        }
        if (sum % 2 == 1) {
            return false;
        }
        int target = sum / 2;
        boolean[][] dp = new boolean[len][target + 1];
        //不选nums[0]
        dp[0][0] = true;
        //选nums[0]
        if (target >= nums[0])
            dp[0][nums[0]] = true;
        for (int i = 1; i < len; i++) {
            for (int j = 0; j <= target; j++) {
                //不选nums[j]
                dp[i][j] = dp[i - 1][j];
                //选nums[j]
                if (j - nums[i] >= 0) {
                    dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i]];
                }
            }

        }
        return dp[len - 1][target];
    }
```

4. 若temp.charAt(0)=='0'只能为一位0不能是001,012,08这种类型.

背包类的动态规划,跟  分割等和子集 是一类题目.  i处选不选物品 由i-1处决定.

```java
改进:背包问题
    对于sum为负数的dp[i][sum],用dp[i][sum+1000]来解决负数
 public int findTargetSumWays(int[] nums, int S) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        //在最底部的return的时候,考虑到数组越界的问题
        if (S < -1000 || S > 1000) {
            return 0;
        }
        int len = nums.length;
        int deta = 1000;
        //realsum + deta  = dp[][x]
        // realSum 为-1000~1000
        //realSum+deta 为 0~2000
        int[][] dp = new int[len][2001];
        //nums[0]=0时,用++ 比用 dp[0][deta - nums[0]]=1要好
        dp[0][deta + nums[0]]++;
        dp[0][deta - nums[0]]++;
        for (int i = 1; i < len; i++) {
            int temp = nums[i];
            for (int j = 0; j <= 2000; j++) {
                if (dp[i - 1][j] != 0) {
                    dp[i][j - nums[i]] += dp[i - 1][j];
                    dp[i][j + nums[i]] += dp[i - 1][j];
                }
            }
        }
        return dp[len - 1][S + deta];
    }
```

#### [560. 和为K的子数组](https://leetcode-cn.com/problems/subarray-sum-equals-k/)

@很容易想到梯度,但是没想到sum-k的形式.

```java
改进: map.put(0,1)放入,是为了sum本身就等于 k,即 sum-k = 0;
		map中先搜索sum-k再放入sum   是为了防止k=0的情况,此时先放入sum-0,再搜索sum,是一种混淆. 本身sum放入map是给下一次累加的sum使用的,而非本次累加.

HashMap<Integer, Integer> map = new HashMap<>();
    public int subarraySum(int[] nums, int k) {
        int count = 0;
        int sum = 0;
        map.put(0, 1);
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            if (map.containsKey(sum - k)) {
                count += map.get(sum - k);
            }
            if (map.containsKey(sum)) {
                map.put(sum, map.get(sum) + 1);
            } else {
                map.put(sum, 1);
            }
        }
        return count;
    }

法2:
public int subarraySum(int[] nums, int k) {
    int left = 0;
    int right = 0;
    int sum = 0;
    int count = 0;
    for (left = 0; left < nums.length; left++) {
        sum = 0;
        for (right = left; right < nums.length; right++) {
            sum += nums[right];
            if (sum == k) {
                count++;
            }
        }
    }
    return count;
}

法3:
 HashMap<Integer, List<Integer>> map = new HashMap<>();

    public int subarraySum(int[] nums, int k) {
        int sum = 0;
        int ret = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            if (map.containsKey(sum)) {
                List<Integer> temp = map.get(sum);
                temp.add(i);
                map.put(sum, temp);
            } else {
                List<Integer> temp = new ArrayList<Integer>();
                temp.add(i);
                map.put(sum, temp);
            }
        }
        sum = 0;
        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(sum + k)) {
                List<Integer> temp = map.get(sum + k);
                for (int num : temp) {
                    if (num >= i) {
                        ret++;
                    }
                }
            }
            sum += nums[i];
        }
        return ret;
    }
```

#### [@221. 最大正方形](https://leetcode-cn.com/problems/maximal-square/)

@ 状态转移的方程也比较好理解. 以i,j作为右下角的正方形的最大边长,是由`  [i-1][j-1] 和[i-1][j]和[i][j-1]`为右下角的三个正方形边长共同决定的.

如图:`dp[2][2] 受限于dp[1][2]=1`

 1   1   0

 1   1    1

 1   1     1

```java
public int maximalSquare(char[][] matrix) {
    int rowBound = matrix.length;
    int colBound = rowBound > 0 ? matrix[0].length : 0;
    int[][] dp = new int[rowBound + 1][colBound + 1];
    int maxSquare = 0;
    for (int i = 1; i <= rowBound; i++) {
        for (int j = 1; j <= colBound; j++) {
            if (matrix[i - 1][j - 1] == '1') {
                dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i - 1][j - 1]), dp[i][j - 1]) + 1;
                maxSquare = Math.max(dp[i][j], maxSquare);
            }

        }
    }
    return maxSquare * maxSquare;
}

//空间优化的写法
 public int maximalSquare(char[][] matrix) {
        int rowBound = matrix.length;
        int colBound = rowBound > 0 ? matrix[0].length : 0;
        int[] dp = new int[colBound + 1];
        int maxSquare = 0;
        int temp = 0, pre = 0;
        for (int i = 1; i <= rowBound; i++) {
            for (int j = 1; j <= colBound; j++) {
                temp = dp[j];
                if (matrix[i - 1][j - 1] == '1') {
                    dp[j] = Math.min(Math.min(dp[j], dp[j - 1]), pre) + 1;
                    maxSquare = Math.max(dp[j], maxSquare);
                } else {
                    // dp[j]在用过多次后,后续不为零,要重新清理
                    dp[j] = 0;
                }
                空间优化的写法,temp,pre的使用
                pre = temp;
            }
        }
        return maxSquare * maxSquare;
    }
```

#### [@34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)

@自己做出来了 但是要更加熟练.

注意的几个点

1.while(left<=right)

2.Lower的时候:return left; Upper的时候: return right;

3.应对没找到返回-1的情况 准备标志位founded.

```java
改进:find不到就返回-1
    getLower 和 getUpper的学习

public int[] searchRange(int[] nums, int target) {
        int left = getLower(nums, target);
        int right = getUpper(nums, target);
        return new int[]{left, right};
    }

    private int getUpper(int[] nums, int target) {
        int low = 0, high = nums.length - 1;
        boolean found = false;
        while (low <= high) {
            int mid = low + (high - low) / 2;
            if (nums[mid] < target) {
                low = mid + 1;
            } else if (nums[mid] == target) {
                low = mid + 1;
                found = true;
            } else if (nums[mid] > target) {
                high = mid - 1;
            }
        }
        return found ? high : -1;
    }

    private int getLower(int[] nums, int target) {
        int low = 0, high = nums.length - 1;
        boolean found = false;
        while (low <= high) {
            int mid = low + (high - low) / 2;
            if (nums[mid] > target) {
                high = mid - 1;
            } else if (nums[mid] == target) {
                high = mid - 1;
                found = true;
            } else if (nums[mid] < target) {
                low = mid + 1;
            }
        }
        return found ? low : -1;
    }
```

#### [240. 搜索二维矩阵 II](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/)

```java
改进:还有一个方法二.

public boolean searchMatrix(int[][] matrix, int target) {
    int rowBound = matrix.length;
    if (rowBound == 0 || matrix[0].length == 0) {
        return false;
    }
    int colBound = matrix[0].length;
    for (int i = rowBound - 1; i >= 0; i--) {
        if (matrix[i][0] == target) {
            return true;
        } else if (matrix[i][0] < target && matrix[i][colBound - 1] >= target) {
            int foundIndex = binarySearch(matrix[i], target);
            if (foundIndex != -1) {
                return true;
            }
        }
    }
    return false;
}

private int binarySearch(int[] nums, int target) {
    int low = 0, high = nums.length - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    return -1;
}

方法二
public boolean searchMatrix(int[][] matrix, int target) {
        // start our "pointer" in the bottom-left
        int row = matrix.length-1;
        int col = 0;

        while (row >= 0 && col < matrix[0].length) {
            if (matrix[row][col] > target) {
                row--;
            } else if (matrix[row][col] < target) {
                col++;
            } else { // found it
                return true;
            }
        }

        return false;
    }
```

#### [@79. 单词搜索](https://leetcode-cn.com/problems/word-search/)

本题只要求返回true,false;但是没法用动规来做

```java
改进:回溯,没啥好说的,继续熟练
boolean[][] visited;
    boolean found;
    int rowBound;
    int colBound;
    int[][] loc = new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public boolean exist(char[][] board, String word) {
        if (board == null || board.length == 0 || board[0].length == 0) {
            return false;
        }
        rowBound = board.length;
        colBound = board[0].length;
        visited = new boolean[rowBound][colBound];

        for (int i = 0; i < rowBound; i++) {
            for (int j = 0; j < colBound; j++) {
                backtracing(board, i, j, 0, word);
            }
        }
        return found;
    }

    private void backtracing(char[][] board, int row, int col, int path, String word) {
        if (found) {
            return;
        }
        if (row < 0 || col < 0 || row >= rowBound || col >= colBound) {
            return;
        }
        if (board[row][col] != word.charAt(path) || visited[row][col]) {
            return;
        }
        if (board[row][col] == word.charAt(path) && path == word.length() - 1) {
            found = true;
            return;
        }
        visited[row][col] = true;
        for (int i = 0; i < loc.length; i++) {
            int rowTemp = row + loc[i][0];
            int colTemp = col + loc[i][1];
            backtracing(board, rowTemp, colTemp, path + 1, word);
        }
        visited[row][col] = false;
    }
```

#### [19. 删除链表的倒数第N个节点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/)

```java
改进:方法很巧妙,有四点值得学
//1.删除节点的题,可以添加一个头结点来用
//2.pre前进N+1步
//3.删除倒数第n个节点,但second到了倒数第n+1就停下,为了使用 second.next = second.next.next;
//4.链表类题目的画图方式  [pHead] [head]  []  []  []  null

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode pHead = new ListNode(1);
    pHead.next = head;
    ListNode pre = pHead;
    ListNode second = pHead;
    for (int i = 1; i <= n + 1; i++) {
        pre = pre.next;//其实应该判空
    }
    while (pre != null) {
        second = second.next;
        pre = pre.next;
    }
    second.next = second.next.next;
    return pHead.next;
}
```

#### [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/)

@跟最长上升子序列一样,dp[i]要初始化成一个值,而非一开始的0.

最长上升子序列,dp[i]初始为1

这道题,如果dp[coins[j]]之前把赋值成1的话,则没有关系.如果之前没有赋值成1,则i的循环开始时,赋值成Integer.MAX_VALUE;代表这个amout无法被找零.

```java
改进:
//动态规划三部演化:  递归(自顶向下)->带备忘录的递归(自顶向下,也就是把递归树剪枝,已经算过的不再重复计算)->动态规划(自底向上)
//背包问题dp[i][j] i是选到第i个物品,j是当前的和,里面的值是
//斐波那契,跳台阶,零钱兑换  dp[i] i是和,是台阶总高,是当前拼凑的总钱数;里面的值是
//里面的值取决于题目,有的是方案可行,有的是方案次数,有的是最优子结构(即最少的拼凑次数,
//最少的跳台阶数,最少的背包物品数)

//我定好base case,同时知道dp[n]是由下面的dp[n-1]等怎么得来的,就可以写出代码

//该代码是下面的优化版本
public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        dp[0] = 0;
        int maxValue = Integer.MAX_VALUE;
        for (int i = 1; i <= amount; i++) {
            dp[i] = maxValue;
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i && dp[i - coins[j]] != maxValue) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] == maxValue ? -1 : dp[amount];
    }

//这个版本可以优化成上面
public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
    	//其实上面的for循环可以省略掉.
        for (int i = 0; i < coins.length; i++) {
            if (coins[i] <= amount) {
                dp[coins[i]] = 1;
            }
        }
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {
            dp[i] = Integer.MAX_VALUE;
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i && dp[i - coins[j]] >= 0 && dp[i - coins[j]] != Integer.MAX_VALUE) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
    }

public int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1];
    //初始化为amount+1是因为一个amount最差也可以由amount个1分硬币构成,所以amount+1是一个不可能的数
    //之所以用amount+1 而不用 -1是因为dp[i] = Math.min(dp[i], dp[i - coin] + 1);是要求最小值
    Arrays.fill(dp, amount + 1);
    dp[0] = 0;
    for (int i = 1; i <= amount; i++) {
        for (int coin : coins) {
            if (coin <= i) {
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }
    return dp[amount] == amount + 1 ? -1 : dp[amount];
}
```

#### [@@@152. 乘积最大子序列](https://leetcode-cn.com/problems/maximum-product-subarray/)

```java
改进:1.状态转移方程
     dp[i][0] = Math.max(imax * nums[i - 1], nums[i - 1]);
     dp[i][1] = Math.min(imin * nums[i - 1], nums[i - 1]);
	2.因为负负得正,所以维护最小值也是有必要的
	3.因为只与前一个dp数据有关,dp数组可以被优化空间

  //首先 dp[i][0]代表以i结尾的数组的最大乘积
    //dp[i][1]代表以i结尾的最小乘积
    //dp[i][0] = Math.max ( dp i-1结尾*nums[i]的值  ,  nums[i])
    //其中, dp i-1结尾*nums[i]的值  ,在nums[i]大于0的情况下  最大值为 dp[i-1][0]*nums[i]
    //在nums[i]小于0的情况下,最大值为dp[i-1][1]*nums[i]
    public int maxProduct(int[] nums) {
        int len = nums.length;
        int[][] dp = new int[len][2];
        dp[0][0] = nums[0];
        dp[0][1] = nums[0];
        int maxRet = nums[0];
        for (int i = 1; i < len; i++) {
            int max = 0;
            int min = 0;
            if (nums[i] > 0) {
                max = dp[i - 1][0] * nums[i];
                min = dp[i - 1][1] * nums[i];
            } else {
                max = dp[i - 1][1] * nums[i];
                min = dp[i - 1][0] * nums[i];
            }
            dp[i][0] = Math.max(max, nums[i]);
            dp[i][1] = Math.min(min, nums[i]);
            maxRet = Math.max(dp[i][0], maxRet);

        }
        return maxRet;
    }

改进:优化空间
    public int maxProduct(int[] nums) {
        int max = Integer.MIN_VALUE, imax = 1, imin = 1;
        for (int i = 0; i < nums.length; i++) {
            int temp = nums[i];
            if (temp < 0) {
                int swap = imax;
                imax = imin;
                imin = swap;
            }
            imax = Math.max(imax * nums[i], nums[i]);
            imin = Math.min(imin * nums[i], nums[i]);
            max = Math.max(imax, max);
        }
        return max;
    }
```

#### [2. 两数相加](https://leetcode-cn.com/problems/add-two-numbers/)

```java
改进:1.巧法:如果l1或者l2为空,则它的值可以看做0,这样就不用后续再对剩下一条非空链做单独处理
    2.见备注的一些细节

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    int num1 = 0;
    int num2 = 0;
    int sum = 0;
    int addOnePre = 0;
    int addOneNow = 0;
    ListNode pHead = new ListNode(1);
    ListNode pre = pHead;
    while (l1 != null || l2 != null) {
        num1 = l1 == null ? 0 : l1.val;
        num2 = l2 == null ? 0 : l2.val;
        sum = num1 + num2;
        if (addOneNow == 1) {
            //先sum++再判断是否超过10
            sum++;
        }
        if (sum >= 10) {
            sum = sum % 10;
            addOnePre = 1;
        } else {
            addOnePre = 0;
        }

        ListNode node = new ListNode(sum);
        pre.next = node;
        pre = pre.next;
        addOneNow = addOnePre;

        l1 = l1 == null ? null : l1.next;
        l2 = l2 == null ? null : l2.next;
    }
    //如果l1和l2都空了,但还有一个进位,也要把这个进位考虑到
    if (addOneNow == 1) {
        pre.next = new ListNode(1);
    }
    return pHead.next;
}
```

#### [98. 验证二叉搜索树](https://leetcode-cn.com/problems/validate-binary-search-tree/)

```java
改进:利用搜索树的性质

boolean ret = true;

public boolean isValidBST(TreeNode root) {
    ArrayList<Integer> list = new ArrayList<>();
    inOrder(root, list);
    return ret;
}

private void inOrder(TreeNode root, ArrayList<Integer> list) {
    if (ret == false) {
        return;
    }
    if (root == null) {
        return;
    }
    inOrder(root.left, list);
    if (list.size() >= 1 && root.val <= list.get(list.size() - 1)) {
        ret = false;
        return;
    } else {
        list.add(root.val);
    }
    inOrder(root.right, list);
}
```

#### [@@31. 下一个排列](https://leetcode-cn.com/problems/next-permutation/)

@思路1后往前找

@思路2 剪枝回溯

思路讲解:https://leetcode-cn.com/problems/next-permutation/solution/xia-yi-ge-pai-lie-by-leetcode/

```java
改进: 从后往前,找到第一个不满足降序的数字 nums[targetIndex]
    再从后往前,找到第一个降序的子数组中比nums[targetIndex]大的nums[i]
	交换nums[targetIndex] 和 nums[i]
    再把targetIndex后面的数变成升序.

    上面的思路有点像 一个数字,比如129,降序数组就是9,把2变成3,129就成了139,然后把降序数组9改成升序数组0
    最终变成了130.

 public void nextPermutation(int[] nums) {
        int changeIndex = -1;
        int swapIndex = 0;
        if (nums == null || nums.length == 0) {
            return;
        }
        //9 8 3 7 6 1 找到从后往前的第一个非升序, 数字3处的索引2
        for (int i = nums.length - 1; i > 0; ) {
            if (nums[i] <= nums[i - 1]) {
                i--;
            } else if (nums[i] > nums[i - 1]) {
                changeIndex = i - 1;
                break;
            }
        }
        //如果已经是最大值,就把nums翻转成最小值
        if (changeIndex == -1) {
            reverse(nums, 0, nums.length - 1);
            return;
        }
        //把3和6交换位置
        for (int i = nums.length - 1; i > 0; i--) {
            if (nums[i] > nums[changeIndex]) {
                swapIndex = i;
                break;
            }
        }
        int temp = nums[swapIndex];
        nums[swapIndex] = nums[changeIndex];
        nums[changeIndex] = temp;
        reverse(nums, changeIndex + 1, nums.length - 1);
        return;
    }

    private void reverse(int[] nums, int left, int right) {
        while (left < right) {
            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
            left++;
            right--;
        }
    }
```

#### 背包问题的理解

#### @@@@@01背包

背包容量是m,一共有n件物品

```
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
```

#### 完全背包

```java
因为每种物品数量无限,所以到了选择第i件物品的时候,要么一件不放(dp[i][j] = dp[i-1][j])要么放1,2,3..k件
dp[i][j] = dp[i][j-weight[i]]+value[i];

所以代码为
for(int i = 1;i<n;i++){
	for(int j = 1;j<=W;j++){
		if(j<weight[i]){
			dp[i][j] = dp[i-1][j];
		}else{
			//要么一个也不放,要么在i物品上能放多少就放多少
			dp[i][j] = Math.max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);
            //其实和多重背包是一样的?k是从0到无限大??  下面是自己的猜测,能不能用做题验证
  			dp[i][j] = Math.max(dp[i-1][j],dp[i][j-weight[i]*k]+value[i]*k);
            dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]*k]+value[i]*k);
		}
	}
}
```

#### 多重背包

```java
每种物品都有数量限制,限制的数值是k,,要么一件不放(dp[i][j] = dp[i-1][j])要么放1,2,3..k件
dp[i][j] = dp[i][j-weight[i]]+value[i];

for(int i = 1;i<n;i++){
	for(int j = 1;j<=W;j++){
		if(j<weight[i]){
			dp[i][j] = dp[i-1][j];
		}else{
			int count = min(num[i],j/weight[i]);
			//要么一个也不放
			dp[i][j] = dp[i-1][j];
			for(int k = 0;k<count;k++){
			//要么1,2...k个都试一下
				dp[i][j] = Math.max(dp[i][j],dp[i-1][j-weight[i]*k]+value[i]*k);
			}
		}
	}
}
```

#### [@@33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/)

```java
改进:做题的一般步骤,理解题意,确认题型,梳理给出的条件
    写代码:  先画出一个比较通用的情况,然后根据情况写代码; 然后再进行边界的处理(有的边界想不到会有致命bug,有的只是逻辑不健全,后者可以在代码通过一般情况后再考虑);然后空间,时间优化,逻辑结构优化

    本题思路,对半查找,有一半递增,有一半增减.检测target和递增的部分首尾的关系,进而决定low或者high接下来的变化

public int search(int[] nums, int target) {
    int len = nums.length;
    int low = 0, high = len - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] == target) {
            return mid;
        }
        if (nums[low] <= nums[mid]) {
            if (nums[low] > target || nums[mid] < target) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        } else {
            if (nums[mid] > target || nums[high] < target) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
    }
    return -1;
}
```

#### [@@3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)

```java
改进:思路滑动窗口
public int lengthOfLongestSubstring(String s) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    int len = s.length();
    int left = 0;
    int right = 0;
    int max = 0;
    HashMap<Character, Integer> charIndexMap = new HashMap<>();
    for (; right < len; right++) {
        char ch = s.charAt(right);
        if (charIndexMap.containsKey(ch)) {
            int charIndex = charIndexMap.get(ch);
            if (charIndex >= left) {
                for (; left <= charIndex; left++) {
                    charIndexMap.remove(s.charAt(left));
                }
            }
        }
        charIndexMap.put(ch, right);
        max = Math.max(max, right - left + 1);
    }
    return max;
}
```

#### [@56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/)

```java
改进:思路 1.按照每个区间的左起点排序,
		2.根据i的right和 i+1的left能否相交决定是否合并区间(right = Math.max(right, intervals[i][1]);
        3.right = Math.max(right, intervals[i][1]);而非right = intervals[i][1],比如(1,9) (2,4)
        还是原来的right大
        4.创建ArrayList<int[]> ret; 转换的时候 ret.toArray(new int[0][]);

public int[][] merge(int[][] intervals) {
        if (intervals == null || intervals.length == 0) {
            return new int[][]{};
        }
        Comparator<int[]> c = new Comparator<int[]>() {
            public int compare(int[] i1, int[] i2) {
                return i1[0] - i2[0];
            }
        };
        int len = intervals.length;
        ArrayList<int[]> ret = new ArrayList<>();
        Arrays.sort(intervals, c);
        for (int i = 0; i < len; ) {
            int start = intervals[i][0];
            int end = intervals[i][1];
            i = i + 1;
            if (i < len && end >= intervals[i][0]) {
                while (i < len && end >= intervals[i][0]) {
                    end = Math.max(end, intervals[i][1]);
                    i++;
                }
            }
            ret.add(new int[]{start, end});
        }
        int[][] retArray = new int[ret.size()][2];
        for (int i = 0; i < ret.size(); i++) {
            retArray[i] = ret.get(i);
        }
        return retArray;
    }
```

#### [@@@300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)

@dp[i]要赋值为1

@状态转移方程 想不到.

比较好的解释:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/man-hua-ti-jie-zai-ben-du-neng-kan-de-dong-de-dong/

```java
  public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length < 1) {
            return 0;
        }
        int len = nums.length;
        int[] dp = new int[len + 1];
        dp[1] = 1;
        int max = 1;
        for (int i = 2; i <= len; i++) {
            dp[i] = 1;
            for (int j = 1; j < i; j++) {
                if (nums[i - 1] > nums[j - 1]) {
                    dp[i] = Math.max(dp[j] + 1, dp[i]);
                    max = Math.max(dp[i], max);
                }
            }
        }
        return max;
    }
```

#### [@@@@@128. 最长连续序列](https://leetcode-cn.com/problems/longest-consecutive-sequence/)

给定一个未排序的整数数组，找出最长连续序列的长度。

要求算法的时间复杂度为 O(n)。

示例:

输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。

@hashMap

@@@注意只有当! map.containsKey(num-1)时,寻找最长连续序列的while循环才开始运行.

```java
思路1:哈希表
    private HashMap<Integer, Integer> map = new HashMap<>();

    public int longestConsecutive(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        for (int temp : nums) {
            map.put(temp, 1);
        }
        int maxLen = 0;
        int curLen = 0;
        for (int temp : nums) {
            if (!map.containsKey(temp - 1)) {
                curLen = 1;
                while (map.containsKey(temp + 1)) {
                    temp++;
                    curLen++;
                }
                maxLen = Math.max(maxLen, curLen);
            }
        }
        return maxLen;
    }

思路2:排序,然后从左到右遍历

public int longestConsecutive(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return 1;
        }
        Arrays.sort(nums);
        //两种排序再熟悉一下
        int right = 1;
        int maxCount = 1;
        int cnt = 1;
        while (right < nums.length) {
            if (nums[right] == nums[right - 1] + 1) {
                cnt++;
                maxCount = Math.max(maxCount, cnt);
                right++;
                while (right < nums.length && nums[right] == nums[right - 1]) {
                    right++;
                }
            } else {
                right++;
                while (right < nums.length && nums[right] == nums[right - 1]) {
                    right++;
                }
                cnt = 1;
            }
        }
        return maxCount;
    }

public int longestConsecutive(int[] nums) {
    if (nums.length == 0) {
        return 0;
    }
    Arrays.sort(nums);
    int max = 1;
    int cnt = 1;
    for (int i = 0; i < nums.length - 1; i++) {
        while (i < nums.length - 1 && nums[i] == nums[i + 1]) {
            i++;
        }
        if (i < nums.length - 1 && nums[i] + 1 == nums[i + 1]) {
            cnt++;
        } else if (i < nums.length - 1 && nums[i] + 1 != nums[i + 1]) {
            cnt = 1;
        }
        max = Math.max(cnt, max);
    }
    return max;
}
```

#### [@23. 合并K个排序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/)

归并排序.再学习.

```java
思路1:分而治之法

 public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }
        return merge(lists, 0, lists.length - 1);
    }

    private ListNode merge(ListNode[] lists, int left, int right) {
        if (left == right) {
            return lists[left];
        }
        int mid = left + (right - left) / 2;
        ListNode l1 = merge(lists, left, mid);
        ListNode l2 = merge(lists, mid + 1, right);
        return mergeTwoList(l1, l2);
    }

    private ListNode mergeTwoList(ListNode l1, ListNode l2) {
        ListNode head = new ListNode(0);
        ListNode pre = head;
        while (l1 != null || l2 != null) {
            if (l1 != null && l2 != null) {
                if (l1.val > l2.val) {
                    pre.next = l2;
                    pre = pre.next;
                    l2 = l2.next;
                } else {
                    pre.next = l1;
                    pre = pre.next;
                    l1 = l1.next;
                }
            } else if (l1 != null) {
                pre.next = l1;
                pre = pre.next;
                l1 = l1.next;
            } else if (l2 != null) {
                pre.next = l2;
                pre = pre.next;
                l2 = l2.next;
            }
        }
        return head.next;
    }

思路2:优先队列

public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) {
        return null;
    }
    ListNode head = new ListNode(1);
    ListNode pre = head;

    PriorityQueue<ListNode> queue = new PriorityQueue<ListNode>(1, (a, b) -> (a.val - b.val));
    for (int i = 0; i < lists.length; i++) {
        if (lists[i] != null) {
            queue.add(lists[i]);
        }
    }
    while (!queue.isEmpty()) {
        ListNode temp = queue.poll();
        pre.next = temp;
        pre = pre.next;
        if (temp.next != null) {
            queue.add(temp.next);
        }
    }
    return head.next;
}
```

#### [124. 二叉树中的最大路径和](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/)

```java
改进思路: 双递归:一层递归求该树的每个节点为root的路径最大值,一层递归求特定root根的左右子树路径的sum
    	注意:如果root.val或者 左右子树+root.val小于零,则return 0; 丢弃该子树

思路1是思路2的改进,把双递归写成了单递归
	int max = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        setMaxAndReturnSingleMax(root);
        return max;
    }

    private int setMaxAndReturnSingleMax(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int leftLen = setMaxAndReturnSingleMax(root.left);
        int rightLen = setMaxAndReturnSingleMax(root.right);
        // 与max比较,设置以该root为根的最长左单边+右单边
        max = Math.max(max, leftLen + rightLen + root.val);
        //获取以root为根的左右单边的最长,返回回去供上一级root使用
        int singleMaxPath = Math.max(leftLen, rightLen) + root.val;
        return Math.max(singleMaxPath, 0);
    }

思路2:
 int max = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        setMax(root);
        return max;
    }
    //获取以root为根的左右两边最长之和,并与max比较
    private void setMax(TreeNode root) {
        if (root == null) {
            return;
        }
        setMax(root.left);
        setMax(root.right);
        int leftLen = gainSingleMaxPath(root.left);
        int rightLen = gainSingleMaxPath(root.right);
        max = Math.max(max, leftLen + rightLen + root.val);
    }
    //获取以root为根的单边(要么左子树,要么右子树)的最长
    private int gainSingleMaxPath(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int leftLen = gainSingleMaxPath(root.left);
        int rightLen = gainSingleMaxPath(root.right);
        int singleMaxPath = Math.max(leftLen, rightLen) + root.val;
        return Math.max(singleMaxPath, 0);
    }

```

#### [@@@239. 滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/)

```java
思路1:双端队列法:

public int[] maxSlidingWindow(int[] nums, int k) {
    ArrayList<Integer> ret = new ArrayList<Integer>();
    Deque<Integer> deque = new ArrayDeque<Integer>();
    int max = 0;
    for (int i = 0; i < nums.length; i++) {
        if (!deque.isEmpty() && deque.getFirst() <= i - k) {
            deque.removeFirst();
        }

        while (!deque.isEmpty() && nums[deque.getLast()] < nums[i]) {
            deque.removeLast();
        }
        deque.addLast(i);
        if (i >= k - 1) {
            ret.add(nums[deque.getFirst()]);
        }
    }
    int[] retArray = new int[ret.size()];
    int i = 0;
    for (int temp : ret) {
        retArray[i++] = temp;
    }
    return retArray;
}

思路2:大顶堆法
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || nums.length < k) {
            return new int[]{};
        }
        Comparator<Integer> c = new Comparator<Integer>() {
            public int compare(Integer i1, Integer i2) {
                return nums[i2] - nums[i1];
            }
        };
        ArrayList<Integer> ret = new ArrayList<>();
        PriorityQueue<Integer> heap = new PriorityQueue<Integer>(10, c);
        int right = 0;
        int len = nums.length;
        for (right = 0; right <= len; right++) {
            if (right < k) {
                heap.add(right);
                continue;
            }
            ret.add(nums[heap.peek()]);
            heap.remove(right - k);
            if (right < len) {
                heap.add(right);
            }
        }
        int[] retArray = new int[ret.size()];
        for (int i = 0; i < ret.size(); i++) {
            retArray[i] = ret.get(i);
        }
        return retArray;
    }

```

#### [@11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/)

@受制于短的,所以每次移动短的.

```java
改进:
思路是双指针法,左右两边的指针往中间移动
public int maxArea(int[] height) {
    int left = 0;
    int right = height.length - 1;
    int maxArea = 0;
    while (left < right) {
        maxArea = Math.max(maxArea, Math.min(height[left], height[right]) * (right - left));
        if (height[left] > height[right]) {
            right--;
        } else {
            left++;
        }
    }
    return maxArea;
}
```

#### [@@42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/)

```java
思路1: 每一个height[i]只有左边和右边分别有比它高的墙才能存住水,
	   所以利用动规,存住i处 左边最高left[i]和右边最高right[i],求min =Math.min(left[i], right[i]);

public int trap(int[] height) {
    if (height == null || height.length == 0) {
        return 0;
    }
    int len = height.length;
    int[] left = new int[len];
    int[] right = new int[len];
    int maxHeight = 0;
    int sum = 0;
    for (int i = 1; i < len; i++) {
        if (height[i - 1] > maxHeight) {
            maxHeight = height[i - 1];
        }
        left[i - 1] = maxHeight;
    }
    maxHeight = 0;
    for (int i = len - 2; i >= 0; i--) {
        if (height[i + 1] > maxHeight) {
            maxHeight = height[i + 1];
        }
        right[i + 1] = maxHeight;
    }
    for (int i = 1; i < len - 1; i++) {
        int min = Math.min(left[i], right[i]);
        if (min > height[i]) {
            sum += min - height[i];
        }
    }
    return sum;
}
```

#### [@@@407. 接雨水 II](https://leetcode-cn.com/problems/trapping-rain-water-ii/)

@思路1 dfs找到流水的出口

@思路2 围起来一圈,逐渐缩小范围

```java
class Solution {
    public int trapRainWater(int[][] heights) {
        if (heights == null || heights.length == 0) return 0;
        int n = heights.length;
        int m = heights[0].length;
        // 用一个vis数组来标记这个位置有没有被访问过
        boolean[][] vis = new boolean[n][m];
        // 优先队列中存放三元组 [x,y,h] 坐标和高度
        PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);

        // 先把最外一圈放进去
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (i == 0 || i == n - 1 || j == 0 || j == m - 1) {
                    pq.offer(new int[]{i, j, heights[i][j]});
                    vis[i][j] = true;
                }
            }
        }
        int res = 0;
        // 方向数组，把dx和dy压缩成一维来
        int[] dirs = {-1, 0, 1, 0, -1};
        while (!pq.isEmpty()) {
            int[] poll = pq.poll();
            // 看一下四个方向
            for (int k = 0; k < 4; k++) {
                int nx = poll[0] + dirs[k];
                int ny = poll[1] + dirs[k + 1];
                // 如果位置合法且没访问过
                if (nx >= 0 && nx < n && ny >= 0 && ny < m && !vis[nx][ny]) {
                    // 如果外围这一圈中最小的比当前这个还高，那就说明能往里面灌水啊
                    if (poll[2] > heights[nx][ny]) {
                        res += poll[2] - heights[nx][ny];
                    }
                    // 如果灌水,高度依然是围墙的最低高度，如果没灌水,最低高度被当前高度取代
                    pq.offer(new int[]{nx, ny, Math.max(heights[nx][ny], poll[2])});
                    vis[nx][ny] = true;
                }
            }
        }
        return res;
    }
}

```

#### [@@@84. 柱状图中最大的矩形](https://leetcode-cn.com/problems/largest-rectangle-in-histogram/)

@@@最小栈法

@width的边界的界定

@哨兵

@持续的pop()最小顶点

```java
@@@最小栈法
public int largestRectangleArea(int[] heights) {
        Stack<Integer> stack = new Stack<Integer>();
        int maxArea = 0;

        int[] newHeights = new int[heights.length + 2];
        for (int i = 1; i < newHeights.length - 1; i++) {
            newHeights[i] = heights[i - 1];
        }
        for (int i = 0; i < newHeights.length; i++) {
            while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
                //为什么pop出来,为什么curWidth是i - (stack.peek() + 1)而不是 i-stack.peek()
                // 举例  有数组2,1,2
                //stack中的1成为rightIndex的时候,stack里面还有 0这一个index
                //以newHeight[1]为高的矩形的最大宽度的左边界  不是1
                // 而是i -(0+1),即 比newHeight[1]小的索引位+1 也就是0+1
                int rightIndex = stack.pop();
                int curHeight = newHeights[rightIndex];

                int curWidth = i - (stack.peek() + 1);
                int area = curWidth * curHeight;
                maxArea = Math.max(maxArea, area);
            }
            stack.push(i);
        }
        return maxArea;
    }

分而治之法:
先找到start 到end 范围内的minHeight;
Area的最大值分三种情况:
1.minIndex左边的部分求出来的最大Area
2.minIndex右边的部分求出来的最大Area
3.minHeight*(end-start+1)

public int calculateArea(int[] heights, int start, int end) {
    if (start > end) {
        return 0;
    }
    //注意这里是以minIndex为分界线.
    int minIndex = start;
    for (int i = start; i <= end; i++) {
        if (heights[minIndex] > heights[i]) {
            minIndex = i;
        }
    }
    int leftMaxArea = calculateArea(heights, start, minIndex - 1);
    int rightMaxArea = calculateArea(heights, minIndex + 1, end);
    int minIndexArea = heights[minIndex] * (end - start + 1);
    return Math.max(minIndexArea, Math.max(leftMaxArea, rightMaxArea));
}

public int largestRectangleArea(int[] heights) {
    if (heights == null || heights.length == 0) {
        return 0;
    }
    return calculateArea(heights, 0, heights.length - 1);
}
```

#### [@@76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/)

@@map初始化的时候,把每种ch都记录一遍出现最大次数    存最大次数的操作要记住

滑动窗口遇到就--.

```java
思路:滑动窗口类的题目,都是left指针+right指针.
    然后先right往右走找到可行解,left往左走,把解优化到最小值.
    重点就在于窗口内的匹配.本题目由于字符串的匹配是无序的,所以用map<Character,Integer> 和 一个mathCount

public String minWindow(String s, String t) {
    //我先不判空,居然通过了
    int matchCount = t.length();
    HashMap<Character, Integer> map = new HashMap<>();
    for (int i = 0; i < t.length(); i++) {
        char c = t.charAt(i);
        if (map.containsKey(c)) {
            map.put(c, map.get(c) + 1);
        } else {
            map.put(c, 1);
        }
    }
    int left = 0;
    int right = 0;
    String minLen = "";
    while (right < s.length()) {
        char temp = s.charAt(right);
        if (map.containsKey(temp)) {
            if (map.get(temp) > 0) {
                matchCount--;
            }
            map.put(temp, map.get(temp) - 1);
        }
        while (matchCount == 0) {
            char temp2 = s.charAt(left);
            if (map.containsKey(temp2)) {
                map.put(temp2, map.get(temp2) + 1);

                if (map.get(temp2) > 0) {
                    if (minLen.length() == 0) {
                        minLen = s.substring(left, right + 1);
                    } else if (minLen.length() > (right - left + 1)) {
                        minLen = s.substring(left, right + 1);
                    }
                    matchCount++;
                }
            }
            left++;
        }
        right++;

    }
    return minLen;
}
```

#### [@@@438. 找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/)

@因为异位词的串的长度跟原字符串大小相同,所以left要跟着right一起走.这样可以避免不必要的讨论.比如abcecba.中间有个e,left如果调到第二个c处,代码会变得复杂

@一开始忘了cnt + map<char,int> 的方法,再做一遍熟练起来

```java
思路:滑动窗口,维护left和right.
    left和right逐渐往右移动,并寻找匹配的字符串.
    注意,此题需要1. s判空,2. 判p的长度要小于s. 有的题会考这种细节,有的不会考

   public static List<Integer> findAnagrams(String s, String p) {
        HashMap<Character, Integer> pCount = new HashMap<>();
        for (int i = 0; i < p.length(); i++) {
            char ch = p.charAt(i);
            pCount.put(ch, pCount.getOrDefault(ch, 0) + 1);
        }
        int pSum = p.length();
        int len = s.length();
        int len2 = p.length();
        int right = 0;
        int left = 0;
        List<Integer> ret = new ArrayList<>();
        while (right < len) {
            char ch = s.charAt(right);
            if (pCount.containsKey(ch)) {
                pCount.put(ch, pCount.get(ch) - 1);
                if (pCount.get(ch) >= 0) {
                    pSum--;
                }
            }
            if (pSum == 0) {
                ret.add(left);
            }

            right++;
            if (right - left + 1 > len2) {
                char temp = s.charAt(left);
                if (pCount.containsKey(temp)) {
                    pCount.put(temp, pCount.get(temp) + 1);
                    if (pCount.get(temp) > 0) {
                        pSum++;
                    }
                }
                left++;
            }
        }
        return ret;
    }

之前的提交:

public List<Integer> findAnagrams(String s, String p) {
    ArrayList<Integer> list = new ArrayList<>();
    HashMap<Character, Integer> map = new HashMap<>();
    int left = 0;
    int right = p.length() - 1;
    int matchCount = p.length();
    if (s == null || s.length() == 0 || p.length() > s.length()) {
        return list;
    }

    for (int i = 0; i < p.length(); i++) {
        char ch = p.charAt(i);
        if (map.containsKey(ch)) {
            map.put(ch, map.get(ch) + 1);
        } else {
            map.put(ch, 1);
        }
    }
    for (int i = 0; i < p.length(); i++) {
        char ch = s.charAt(i);
        if (map.containsKey(ch)) {
            if (map.get(ch) > 0) {
                matchCount--;
            }
            map.put(ch, map.get(ch) - 1);
        }
    }
    while (right < s.length()) {
        if (matchCount == 0) {
            list.add(left);
        }
        //左右都移动
        char ch = s.charAt(left);
        if (map.containsKey(ch)) {
            map.put(ch, map.get(ch) + 1);
            if (map.get(ch) > 0) {
                matchCount++;
            }
        }
        left++;
        right++;
        if (right < s.length()) {
            ch = s.charAt(right);
            if (map.containsKey(ch)) {
                if (map.get(ch) > 0) {
                    matchCount--;
                }
                map.put(ch, map.get(ch) - 1);
            }
        }
    }
    return list;
}
```

#### [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/)

```java
思路3个:
1.dfs
2.bfs
3.查并集

 //   dfs:

 private boolean[][] marked;
    private int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    char[][] grid;
    int row = 0;
    int col = 0;
    int count = 0;

    public int numIslands(char[][] grid2) {
        grid = grid2;
        if (grid == null || grid.length == 0) {
            return 0;
        }
        row = grid.length;
        col = grid[0].length;
        marked = new boolean[row][col];

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (isHealthy(i, j)) {
                    dfs(i, j);
                    count++;
                }
            }
        }
        return count;
    }

    private boolean isHealthy(int row, int col) {
        if (row < 0 || col < 0 || row > grid.length - 1 || col > grid[0].length - 1 || marked[row][col] || grid[row][col] == '0') {
            return false;
        }
        return true;
    }

    private void dfs(int row, int col) {
        if (isHealthy(row, col)) {
            marked[row][col] = true;
            for (int i = 0; i < directions.length; i++) {
                int newRow = row + directions[i][0];
                int newCol = col + directions[i][1];
                dfs(newRow, newCol);
            }
        }
    }

//bfs 但是会超时,
   private boolean[][] marked;
    private int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    char[][] grid;
    int row = 0;
    int col = 0;
    int count = 0;

    public int numIslands(char[][] grid2) {
        grid = grid2;
        if (grid == null || grid.length == 0) {
            return 0;
        }
        row = grid.length;
        col = grid[0].length;
        marked = new boolean[row][col];
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (isHealthy(i, j)) {
                    Queue<Location> queue = new LinkedList<>();
                    Location loc = new Location(i, j);
                    queue.add(loc);
                    count++;
                    while (!queue.isEmpty()) {
                        Location temp = queue.poll();
                        int X = temp.x;
                        int Y = temp.y;
                        marked[X][Y] = true;
                        for (int k = 0; k < directions.length; k++) {
                            int newX = X + directions[k][0];
                            int newY = Y + directions[k][1];
                            if (isHealthy(newX, newY)) {
                                queue.add(new Location(newX, newY));
                            }
                        }
                    }
                }
            }
        }
        return count;
    }

    private boolean isHealthy(int row, int col) {
        if (row < 0 || col < 0 || row > grid.length - 1 || col > grid[0].length - 1 || marked[row][col] || grid[row][col] == '0') {
            return false;
        }
        return true;
    }

class Location {
    Integer x;
    Integer y;

    Location(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

//并查集.

//TODO

```

#### [@@@@139. 单词拆分](https://leetcode-cn.com/problems/word-break/)

这个题是一个背包问题.index往后面遍历,决定某个index是否符合要求的原因是index-str.len  ~index是否能构成一个单词

for(;index<len;index++){

      for(String temp1:wordDict){

@@@这个题 index往后遍历,

```java
本来想用回溯的办法,但是像这种只要求一个boolean结果或者一个int结果的
一般用回溯就会超时.

所以应该用动态规划代替.

dp[j] = dp[j-len]
比如  leetcode  与 leet code   dp[7] = dp[3]&&(s的code==wordDict里面的code)

 public boolean wordBreak(String s, List<String> wordDict) {
        if (s == null || s.length() == 0) {
            return false;
        }
        int len = s.length();
        boolean[] dp = new boolean[len + 1];
        dp[0] = true;
        for (int i = 1; i <= len; i++) {
            for (int j = 0; j < wordDict.size(); j++) {
                String str = wordDict.get(j);
                int strLen = str.length();
                if (i - strLen >= 0 && dp[i - strLen] == true) {
                    if (str.equals(s.substring(i - strLen, i))) {
                        dp[i] = true;
                        break;
                    }
                }
            }
        }
        return dp[len];
    }

public boolean wordBreak(String s, List<String> wordDict) {
    if (s == null || s.length() == 0) {
        return false;
    }
    HashSet<String> set = new HashSet<>(wordDict);
    int len = s.length();
    boolean[] dp = new boolean[len];
    String temp = "";
    int index = 0;

    //初始化dp[0]到第一个为true的dp.
    for (index = 0; index < len; index++) {
        temp = s.substring(0, index + 1);
        if (set.contains(temp)) {
            dp[index] = true;
            break;
        } else {
            dp[index] = false;
        }
    }
    //从index开始
    for (; index < len; index++) {
        //每一个wordDict里面的单词都试一下,能不能从index开始
        //往前匹配成功,并且dp[index-length]为true;
        for (String temp2 : wordDict) {
            int length = temp2.length();
            if (length - 1 <= index) {
                String temp3 = s.substring(index - length + 1, index + 1);
                if (temp2.equals(temp3)) {
                    if (index - length == -1) {
                        dp[index] = true;
                        break;
                    } else if (dp[index - length]) {
                        dp[index] = true;
                        break;
                    }
                }
            }
        }
    }
    return dp[index - 1];
}
```

#### [55. 跳跃游戏](https://leetcode-cn.com/problems/jump-game/)

```java
思路:动态规划

public boolean canJump(int[] nums) {
    if (nums == null || nums.length == 0) {
        return false;
    }
    int len = nums.length;
    boolean[] dp = new boolean[len];
    dp[0] = true;
    for (int i = 1; i < len; i++) {
        for (int j = i - 1; j >= 0; j--) {
            if (dp[j] && nums[j] >= i - j) {
                dp[i] = true;
                break;
            }
        }
    }
    return dp[len - 1];
}
```

#### [@@662. 二叉树最大宽度](https://leetcode-cn.com/problems/maximum-width-of-binary-tree/)

@@遍历每一个node,当level相同时,就计算breadth

如果level不相同,就更新cutlevel和left

```java
    public int widthOfBinaryTree(TreeNode root) {
        if (root == null) {
            return 0;
        }
        Queue<Node> queue = new LinkedList<Node>();
        queue.add(new Node(root, 0, 0));
        int left = 0;
        int right = 0;
        int curLevel = 0;
        int maxB = 0;
        int breadth = 0;
        while (!queue.isEmpty()) {
            Node node = queue.poll();
            addNodeChild(queue, node);
            int nodeLevel = node.level;
            int nodePosition = node.position;
            //当处在同一个level下实时计算breadth
            if (curLevel == nodeLevel) {
                right = nodePosition;
                breadth = right - left + 1;
                maxB = Math.max(maxB, breadth);
                //当level变更时,更换curLevel和left
            } else {
                left = nodePosition;
                curLevel = nodeLevel;
            }
        }
        return maxB;
    }

    private void addNodeChild(Queue<Node> queue, Node node) {
        TreeNode treeNode = node.treeNode;
        int level = node.level;
        int position = node.position;

        TreeNode left = treeNode.left;
        TreeNode right = treeNode.right;
        if (left != null) {
            queue.add(new Node(left, position * 2, level + 1));
        }
        if (right != null) {
            queue.add(new Node(right, position * 2 + 1, level + 1));
        }
    }

class Node {
    TreeNode treeNode = null;
    int position = 0;
    int level = 0;

    public Node(TreeNode treeNode, int position, int level) {
        this.treeNode = treeNode;
        this.position = position;
        this.level = level;
    }
}
```

#### [@15. 三数之和](https://leetcode-cn.com/problems/3sum/)

@这个题对于重复数据的处理很难,题目的难度是困难而非中等.

@@对于三数求和,可以用排序+左右指针,注意对于每个nums[i]只需要从其右边的数里面选nums[left]和 nums[right]

@回溯法对结果去重比较麻烦,本题专有法来做.

```java
改进:1.数组排序
    1. 以i为左起点,nums[i]大于0则可以剪枝,否则L++R--来寻找组合
    3.num[i]可能会有重复,要用最左边的num[i],不然比如 -1  -1 0 1 2,用最右边的nums[i]会漏掉 -1 -1 2 这种情况
    4.找到 num[i] nums[L] nums[R]的组合之后  L要一直++ R要一直--,直到nums[L]!=nums[L],为的是去除重复解
    比如 -2 -1 -1 0 3 3

public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ret = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {

            if (nums[i] > 0) {
                return ret;
            }
            int left = i + 1;
            int right = nums.length - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    List<Integer> list = new ArrayList<>();
                    list.add(nums[i]);
                    list.add(nums[left]);
                    list.add(nums[right]);
                    ret.add(list);
                    while (left + 1 < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    while (left < right - 1 && nums[right] == nums[right - 1]) {
                        right--;
                    }
                    left++;
                    right--;
                } else if (sum > 0) {
                    right--;
                } else {
                    left++;
                }

            }
            while (i + 1 < nums.length && nums[i] == nums[i + 1]) {
                i++;
            }
        }
        return ret;
    }
@这个题很坑的是 回溯法不好去重,所以还是要用上面的排序法.
测试用例:
输入
[-1,0,1,2,-1,-4]
输出
[[-1,0,1],[-1,2,-1],[0,1,-1]]
预期结果
[[-1,-1,2],[-1,0,1]]

List<List<Integer>> ret = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> threeSum(int[] nums) {
        backtracing(nums, new ArrayList<>(), 0, 0);
        return ret;
    }

    private void backtracing(int[] nums, List<Integer> path, int sum, int index) {
        if (path.size() > 3) {
            return;
        }
        if (path.size() == 3 && sum == 0) {
            ret.add(new ArrayList<>(path));
            return;
        }
        for (int i = index; i < nums.length; i++) {
            path.add(nums[i]);
            backtracing(nums, path, sum + nums[i], i + 1);
            path.remove(path.size() - 1);
        }
    }
```

#### @归并排序(mergeSort)

结合128题.最长连续序列  里面的思路二,来验证排序

```java
public void mergeSort(int[] nums) {
        merge(nums, 0, nums.length - 1);
    }

    private void merge(int[] nums, int left, int right) {
        if (left >= right) {
            return;
        }
        //当时没想到临时数组
        int[] temp = new int[right - left + 1];
        int mid = left + (right - left) / 2;
        merge(nums, left, mid);
        merge(nums, mid + 1, right);

        //把nums的  left~mid   和  mid+1~right 合并到temp

        int i1 = left;
        int i2 = mid + 1;
        int i = 0;
        while (i1 <= mid && i2 <= right) {
            if (nums[i1] <= nums[i2]) {
                temp[i++] = nums[i1++];
            } else {
                temp[i++] = nums[i2++];
            }
        }
        while (i1 <= mid) {
            temp[i++] = nums[i1++];
        }
        while (i2 <= right) {
            temp[i++] = nums[i2++];
        }
        //把合并后的temp转移到nums
        i1 = left;
        for (i = 0; i < temp.length; ) {
            nums[i1++] = temp[i++];
        }
    }
```

#### @快速排序(quickSort)

结合128题.最长连续序列  里面的思路二,来验证排序

```java
 private void quickSort(int[] nums, int left, int right) {
        if (left >= right) {
            return;
        }
        int index = patition(nums, left, right);
        quickSort(nums, left, index - 1);
        quickSort(nums, index + 1, right);
    }

    private int patition(int nums[], int left, int right) {
        if (left >= right) {
            return left;
        }
        int temp = nums[left];
        while (left < right) {
            //用left<right 而不是left<=right是为了防止left越过right,swap的时候越界
            //left<right反而会让left和right最终重合
            while (left < right && nums[right] >= temp) {
                right--;
            }
            swap(nums, left, right);
            //nums[left]和 temp比较的时候用<=,而非< 为了让数组更快的总到尽头
            while (left < right && nums[left] <= temp) {
                left++;
            }
            swap(nums, left, right);
        }
        //返回left没有疑问,示例如下
        // 1,2,3,4,5    3,1,2,4,5
        nums[left] = temp;
        return left;
    }

    private void swap(int[] nums, int left, int right) {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
    }
```

#### @@@堆排序

 @sort里面新建maxHeap,然后把最后一个元素移到末尾,adjust

@maxHeap里面从最后一个非子节点(len/2-1)开始调整,一直到0.

@调整函数里面,从index里面第一个子节点一直往下调整,最后temp放到应该在的位置上.

```java
public void sort(int[] nums) {
    int[] ints = maxHeap(nums);
    for (int endLen = nums.length; endLen > 0; endLen--) {
        //把根元素和末尾元素交换位置
        swap(nums, 0, endLen - 1);//endLen-1是index
        //以根元素 到 len = endLen-1的堆进行调整  (忽略了最后的末尾元素)
        adjustHeap(nums, 0, endLen - 1);//endLen-1是 len
    }
}

public int[] maxHeap(int[] nums) {
        //从第一个非叶子节点开始   len/2-1  到 根节点
        for (int i = nums.length / 2 - 1; i >= 0; i--) {
            adjustHeap(nums, i, nums.length);
        }
        return nums;
}

//在array里面,左子树为index*2+1,右子树为index*2+2
//在每一层position那里
private void adjustHeap(int[] nums, int index, int len) {
    int temp = nums[index];

    for (int childIndex = index * 2 + 1; childIndex < len; childIndex = childIndex * 2 + 1) {
        if (childIndex + 1 < len && nums[childIndex] < nums[childIndex + 1]) {
            childIndex++;
        }
        if (temp < nums[childIndex]) {
            nums[index] = nums[childIndex];
            index = childIndex;
        } else {
            break;
        }
    }
    nums[index] = temp;
}

private void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}
```

#### @@但凡是有关left,right的函数,一般要判断left>right return

#### [面试题42. 连续子数组的最大和](https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/)

```java
public int maxSubArray(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    int len = nums.length;
    int[] dp = new int[len + 1];
    dp[1] = nums[0];

    int max = nums[0];
    for (int i = 2; i <= len; i++) {
        dp[i] = Math.max(dp[i - 1] + nums[i - 1], nums[i - 1]);
        max = Math.max(dp[i], max);
    }
    return max;
}
```

#### [面试题03. 数组中重复的数字](https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/)

```java
public int findRepeatNumber(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    int len = nums.length;
    int index = 0;
    while (index < len) {
        int num = nums[index];
        if (num != index) {
            if (nums[num] == num) {
                return num;
            }
            int temp = nums[num];
            nums[num] = num;
            nums[index] = temp;
        } else {
            index++;
        }
    }
    return len;
}
```

#### [@@@@32. 最长有效括号](https://leetcode-cn.com/problems/longest-valid-parentheses/)

@stack初始push一个-1

@  (   进栈

@ )  出栈,如果stack空了,代表)多了,再把)的indexpush进去,作为分界线.

@如果没空,代表匹配成功了

```java
public int longestValidParentheses(String s) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    int len = s.length();
    int maxLen = 0;
    Stack<Integer> stack = new Stack<Integer>();
    stack.push(-1);
    for (int i = 0; i < len; i++) {
        char ch = s.charAt(i);
        if (ch == '(') {
            stack.push(i);
        } else {
            stack.pop();
            if (stack.isEmpty()) {
                stack.push(i);
            } else {
                maxLen = Math.max(maxLen, i - stack.peek());
            }
        }
    }
    return maxLen;
}
```

#### [@935. 骑士拨号器](https://leetcode-cn.com/problems/knight-dialer/)

```java
public int knightDialer(int N) {
    if (N < 0) {
        return -1;
    }
    if (N == 1) {
        return 10;
    }
    boolean[][] cannotUse = new boolean[5][4];
    cannotUse[4][1] = true;
    cannotUse[4][3] = true;
    int[][][] dp = new int[5][4][N + 1];
    int[][] loc = new int[][]{{1, 2}, {1, -2}, {-1, 2}, {-1, -2}, {2, 1}, {2, -1}, {-2, 1}, {-2, -1}};
    for (int i = 1; i <= 4; i++) {
        for (int j = 1; j <= 3; j++) {
            for (int locIndex = 0; locIndex < loc.length; locIndex++) {
                int newI = i + loc[locIndex][0];
                int newJ = j + loc[locIndex][1];
                if (newI >= 1 && newI <= 4 && newJ >= 1 && newJ <= 3 && cannotUse[newI][newJ] == false) {
                    dp[i][j][1]++;
                }
            }
        }
    }
    int sum = 0;
    for (int k = 2; k <= N - 1; k++) {
        for (int i = 1; i <= 4; i++) {
            for (int j = 1; j <= 3; j++) {
                for (int locIndex = 0; locIndex < loc.length; locIndex++) {
                    int newI = i + loc[locIndex][0];
                    int newJ = j + loc[locIndex][1];
                    if (newI >= 1 && newI <= 4 && newJ >= 1 && newJ <= 3 && cannotUse[newI][newJ] == false) {

                        dp[i][j][k] += dp[newI][newJ][k - 1];
                        dp[i][j][k] = dp[i][j][k] % 1000000007;
                    }
                }
            }
        }
    }
    for (int i = 1; i <= 4; i++) {
        for (int j = 1; j <= 3; j++) {
            if (cannotUse[i][j] == false) {
                sum += dp[i][j][N - 1];
                sum = sum % (1000000007);
            }
        }
    }
    return sum;
}
```

#### [@@1353. 最多可以参加的会议数目](https://leetcode-cn.com/problems/maximum-number-of-events-that-can-be-attended/)

@把events 根据开始顺序排序,today逐渐增长,从优先队列里,选剩余时间最少的会议来开.

```java
public int maxEvents(int[][] events) {
    int MAX_END = 100000;
    if (events == null || events.length == 0) {
        return 0;
    }
    Comparator<int[]> c = new Comparator<int[]>() {
        public int compare(int[] i1, int[] i2) {
            return i1[0] - i2[0];
        }
    };
    Arrays.sort(events, c);

    PriorityQueue<Integer> queue = new PriorityQueue<Integer>();

    int meetCount = 0;
    int i = 0;
    for (int today = 1; today <= MAX_END; today++) {
        while (i < events.length && events[i][0] == today) {
            queue.add(events[i][1]);
            i++;
        }
        if (!queue.isEmpty() && queue.peek() >= today) {
            meetCount++;
            queue.poll();
        }
        while (!queue.isEmpty() && queue.peek() <= today) {
            queue.poll();
        }
    }
    return meetCount;
}
```

#### [@25. K 个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group/)

```java
public ListNode reverseKGroup(ListNode head, int k) {
    ListNode node = head;
    ListNode retHead = new ListNode(-1);
    ListNode retEnd = retHead;
    while (true) {
        int count = 0;
        ListNode reverseHead = new ListNode(-1);
        ListNode lastNode = null;
        //注意记录lastNode,是count为0的时候
        while (node != null && count < k) {
            ListNode temp = node.next;
            node.next = reverseHead.next;
            reverseHead.next = node;
            //注意记录lastNode,是count为0的时候
            if (count == 0) {
                lastNode = node;
            }
            node = temp;
            count++;
        }

        if (count == k) {
            retEnd.next = reverseHead.next;
            retEnd = lastNode;
        } else {
            //如果最后的链表不足k个,就翻转回来原来的顺序
            ListNode remainList = reverse(reverseHead);
            retEnd.next = remainList;
            break;
        }
    }
    return retHead.next;
}

private ListNode reverse(ListNode head) {
    ListNode pHead = new ListNode(-1);
    ListNode node = head.next;
    while (node != null) {
        ListNode temp = node.next;
        node.next = pHead.next;
        pHead.next = node;
        node = temp;
    }
    return pHead.next;
}
```

#### [@@@721. 账户合并](https://leetcode-cn.com/problems/accounts-merge/)

同一个邮箱代表是同一个用户。

我们用输入账户的序号来代表账户。

例如：

[
0 [“John”,”<EMAIL>”,”<EMAIL>”],
1 [“John”,”<EMAIL>”,”<EMAIL>”],
2 [“Mary”,”<EMAIL>”],
3 [“John”,”<EMAIL>”]
]

则有4个账户。

用哈希表建立<邮箱,账户1,…,账户x>的关联。

“<EMAIL>” 0 1
“<EMAIL>” 0
“<EMAIL>” 1
“<EMAIL>” 2
“<EMAIL>” 3

对账户建立并查集。同一个邮箱的账户属于同一类。

则分成三类：{0,1};{2};{3}。

getParent()  为1  则在 hashmap  1,List<mail>上面添加邮箱

最后把map<nameId 1 , list<mail>> 变成 list<list<String>>

将同一类的账户和邮箱收集即可。

[
[“John”,”<EMAIL>”,”<EMAIL>”,”<EMAIL>”],
[“Mary”,”<EMAIL>”],
[“John”,”<EMAIL>”]
]

```
整体思路：

hash<邮箱,账户1,...,账户x>;
P:并查集;
P[i] = i;
for([账户i,邮箱i1,...,邮箱it] in accounts){
    for(邮箱ij in [邮箱i1,...,邮箱it]){
        hash[邮箱ij] = [...] + 账户i;
    }
}
//合并同类
for(<邮箱ij,账户i,...账户u] in hash){
    for(账户x in [账户i,...账户u]){
        p1 = find(P,账户x);
        p2 = find(P,账户i);
        P[p1] = P2;
    }
}
将同一类账户的邮箱收集起来即可;
```

#### [@@@301. 删除无效的括号](https://leetcode-cn.com/problems/remove-invalid-parentheses/)

```java
class Solution {
public:
    vector<string> ans;
    vector<string> removeInvalidParentheses(string s) {
        // 寻找不合法半边括号的个数
        int left=0;
        int right=0;
        for(char i:s){
            if(i=='('){
                left++;
            }
            if(i==')'){
                if(left>0)left--;
                else right++;
            }
        }
        // 深度优先遍历，寻找题解
        // left与right为需要删除的数量
        dfs(s, 0, left, right);

        return ans;
    }
    void dfs(string s, int st, int l, int r){
        if(l==0&&r==0){
            if(check(s)){
                ans.push_back(s);
            }
            return;
        }
        for(int i=st;i<s.size();i++){
             // 去重
            if(i-1>=st&&s[i]==s[i-1])continue;
            if(l>0&&s[i]=='('){
                dfs(s.substr(0, i)+s.substr(i+1, s.size()-i-1), i, l-1, r);
            }
            if(r>0&&s[i]==')'){
                dfs(s.substr(0, i)+s.substr(i+1, s.size()-i-1), i, l, r-1);
            }
        }
    }
    // 检验合法性
    bool check(string s){
        int cnt=0;
        for(char i:s){
            if(i=='('){
                cnt++;
            }
            if(i==')'){
                cnt--;
                if(cnt<0)return false;
            }
        }
        return cnt==0;
    }
};
```

<file_sep>/README.md
# Introduction
平时学习积累的知识笔记 ↓

##  Java

- [Java 基础](https://github.com/sx89/Java-notes/blob/master/notes/Java-%E5%9F%BA%E7%A1%80.md)
- [Java 集合](https://github.com/sx89/Java-notes/blob/master/notes/Java-%E9%9B%86%E5%90%88.md)
- [Java 虚拟机](https://github.com/sx89/Java-notes/blob/master/notes/Java-%E8%99%9A%E6%8B%9F%E6%9C%BA.md)
- [Java 并发](https://github.com/sx89/Java-notes/blob/master/notes/Java-%E5%B9%B6%E5%8F%91.md)
- [Java I/O](https://github.com/sx89/Java-notes/blob/master/notes/Java-IO.md)
- [Java 设计模式](https://github.com/sx89/Java-notes/blob/master/notes/Java-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md)
- [设计模式](https://github.com/sx89/Java-notes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md)
- [面向对象思想](https://github.com/sx89/Java-notes/blob/master/notes/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%80%9D%E6%83%B3.md)

## 算法

- [剑指Offer](https://github.com/sx89/Java-notes/blob/master/notes/%E5%89%91%E6%8C%87offer.md)
- [LeetCode01](https://github.com/sx89/Java-notes/blob/master/notes/LeetCode01.md)
- [LeetCode02](https://github.com/sx89/Java-notes/blob/master/notes/LeetCode02.md)
- [LeetCode03](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/LeetCode03.md)
- [LeetCode04](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/LeetCode04.md)
- [排序算法总结](https://github.com/sx89/Java-notes/blob/master/notes/%E7%AE%97%E6%B3%95-%E6%8E%92%E5%BA%8F.md)

##  MySQL数据库

- [MySQL与数据库原理](https://github.com/sx89/Java-notes/blob/master/notes/MYSQL.md)
- [SQL语句](https://github.com/sx89/Java-notes/blob/master/notes/MYSQL.md)

##  分布式框架

- [分布式基础知识](https://github.com/sx89/Java-notes/blob/master/notes/%E5%88%86%E5%B8%83%E5%BC%8F.md)
- [开发框架-SpringBoot](https://github.com/sx89/summary-of-all-my-repositories)
- [高速缓存-Redis](https://github.com/sx89/Java-notes/blob/master/notes/redis.md)
- [消息中间件-RocketMQ](https://github.com/sx89/rocketmq4.0-code)
- [百万连接-Netty](https://github.com/sx89/network_and_protocol)
- [容器技术-Docker](https://github.com/sx89/docker-note)
- [消息中间件-Kafka](https://github.com/sx89/Java-notes/blob/master/notes/Kafka.md)

##  操作系统

- [计算机操作系统](https://github.com/sx89/Java-notes/blob/master/notes/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.md)

## 计算机网络

- [计算机网络](https://github.com/sx89/Java-notes/blob/master/notes/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C.md)
- [HTTP](https://github.com/sx89/Java-notes/blob/master/notes/HTTP.md)
- [Socket](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Socket.md)

## Golang

- [Golang 基础](https://github.com/sx89/Java-notes/blob/master/notes/Golang.md)
- [b站轻视频服务端开发](https://github.com/sx89/Java-notes/blob/master/notes/%E8%A7%86%E9%A2%91%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F.md)

## Linux

- [Linux](https://github.com/sx89/Java-notes/blob/master/notes/Linux.md)
- [Shell]()

## 大数据（刚开始，笔记不全）

- [Hadoop](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Hadoop.md)
- [Zookeeper](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Zookeeper.md)
- [Spark](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Spark.md)
- [Scala](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Scala.md)
- [Hive](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Hive.md)
- [Hbase](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Hbase.md)
- [Phoenix](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Phoenix.md)
- [Flink](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Flink.md)
- [Flume](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Flume.md)
- [Kafka](https://github.com/sx89/Knowledge-Catalogue/blob/master/notes/Kafka.md)

<file_sep>/notes/Flink.md
## Flink的重要特点

### 2.1 事件驱动型(Event-driven)

事件驱动型应用是一类具有状态的应用，它从一个或多个事件流提取数据，并根据到来的事件触发计算、状态更新或其他外部动作。比较典型的就是以kafka为代表的消息队列几乎都是事件驱动型应用。

与之不同的就是SparkStreaming微批次，如图：

### 流与批的世界观

   **批处理**的特点是有界、持久、大量，非常适合需要访问全套记录才能完成的计算工作，一般用于离线统计。

**流处理**的特点是无界、实时,  无需针对整个数据集执行操作，而是对通过系统传输的每个数据项执行操作，一般用于实时统计。

  **在spark的世界观中**，一切都是由批次组成的，离线数据是一个大批次，而实时数据是由一个一个无限的小批次组成的。

  **而在flink的世界观中**，一切都是由流组成的，离线数据是有界限的流，实时数据是一个没有界限的流，这就是所谓的有界流和无界流。

**无界数据流**：**无界数据流有一个开始但是没有结束**，它们不会在生成时终止并提供数据，必须连续处理无界流，也就是说必须在获取后立即处理event。对于无界数据流我们无法等待所有数据都到达，因为输入是无界的，并且在任何时间点都不会完成。处理无界数据通常要求以特定顺序（例如事件发生的顺序）获取event，以便能够推断结果完整性。

**有界数据流**：**有界数据流有明确定义的开始和结束**，可以在执行任何计算之前通过获取所有数据来处理有界流，处理有界流不需要有序获取，因为可以始终对有界数据集进行排序，有界流的处理也称为批处理。

### 分层api

最底层级的抽象仅仅提供了有状态流，它将通过过程函数（Process Function）被嵌入到DataStream API中。底层过程函数（Process Function） 与 DataStream API 相集成，使其可以对某些特定的操作进行底层的抽象，它允许用户可以自由地处理来自一个或多个数据流的事件，并使用一致的容错的状态。除此之外，用户可以注册事件时间并处理时间回调，从而使程序可以处理复杂的计算。

实际上，**大多数应用并不需要上述的底层抽象，而是针对核心****API****（Core APIs****）** **进行编程，比如DataStream API****（有界或无界流数据）以及DataSet API****（有界数据集）**。这些API为数据处理提供了通用的构建模块，比如由用户定义的多种形式的转换（transformations），连接（joins），聚合（aggregations），窗口操作（windows）等等。DataSet API 为有界数据集提供了额外的支持，例如循环与迭代。这些API处理的数据类型以类（classes）的形式由各自的编程语言所表示。

Table API 是以表为中心的声明式编程，其中表可能会动态变化（在表达流数据时）。Table API遵循（扩展的）关系模型：表有二维数据结构（schema）（类似于关系数据库中的表），同时API提供可比较的操作，例如select、project、join、group-by、aggregate等。Table API程序声明式地定义了什么逻辑操作应该执行，而不是准确地确定这些操作代码的看上去如何 。 尽管Table API可以通过多种类型的用户自定义函数（UDF）进行扩展，其仍不如核心API更具表达能力，但是使用起来却更加简洁（代码量更少）。除此之外，Table API程序在执行之前会经过内置优化器进行优化。

**你可以在表与 DataStream/DataSet** **之间无缝切换，以允许程序将 Table API** **与 DataStream** **以及 DataSet** **混合使用**。

Flink提供的最高层级的抽象是 SQL 。这一层抽象在语法与表达能力上与 Table API 类似，但是是以SQL查询表达式的形式表现程序。SQL抽象与Table API交互密切，同时SQL查询可以直接在Table API定义的表上执行。

### 2.4 支持有状态计算

Flink在1.4版本中实现了状态管理，所谓状态管理就是在流失计算过程中将算子的中间结果保存在内存或者文件系统中，等下一个事件进入算子后可以让当前事件的值与历史值进行汇总累计。

### 2.5 支持exactly-once语义

在分布式系统中，组成系统的各个计算机是独立的。这些计算机有可能fail。

一个sender发送一条message到receiver。根据receiver出现fail时sender如何处理fail，可以将message delivery分为三种语义:

**At Most once:** 对于一条message,receiver最多收到一次(0次或1次).

可以达成At Most Once的策略:

sender把message发送给receiver.无论receiver是否收到message,sender都不再重发message.

**At Least once:** 对于一条message,receiver最少收到一次(1次及以上).

可以达成At Least Once的策略:

sender把message发送给receiver.当receiver在规定时间内没有回复ACK或回复了error信息,那么sender重发这条message给receiver,直到sender收到receiver的ACK.

**Exactly once:** 对于一条message,receiver确保只收到一次

### 2.6  支持事件时间（EventTime）

  目前大多数框架时间窗口计算，都是采用当前系统时间，以时间为单位进行的聚合计算只能反应数据到达计算引擎的时间，而并不是实际业务时间

#  Flink运行架构

## 任务提交流程（yarn模式）

图 Yarn模式任务提交流程

Flink任务提交后，Client向HDFS上传Flink的Jar包和配置，之后向Yarn ResourceManager提交任务，ResourceManager分配Container资源并通知对应的NodeManager启动ApplicationMaster，ApplicationMaster启动后加载Flink的Jar包和配置构建环境，然后启动JobManager，之后ApplicationMaster向ResourceManager申请资源启动TaskManager，ResourceManager分配Container资源后，由ApplicationMaster通知资源所在节点的NodeManager启动TaskManager，NodeManager加载Flink的Jar包和配置构建环境并启动TaskManager，TaskManager启动后向JobManager发送心跳包，并等待JobManager向其分配任务。

## 任务调度原理

客户端不是运行时和程序执行的一部分，但它用于准备并发送dataflow(JobGraph)给Master(JobManager)，然后，客户端断开连接或者维持连接以等待接收计算结果。

 当 Flink 集群启动后，首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager，JobManager 再调度任务到各个 TaskManager 去执行，然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。

**Client** 为提交 Job 的客户端，可以是运行在任何机器上（与 JobManager 环境连通即可）。提交 Job 后，Client 可以结束进程（Streaming的任务），也可以不结束并等待结果返回。

**JobManager** 主要负责调度 Job 并协调 Task 做 checkpoint，职责上很像 Storm 的 Nimbus。从 Client 处接收到 Job 和 JAR 包等资源后，会生成优化后的执行计划，并以 Task 的单元调度到各个 TaskManager 去执行。

**TaskManager** 在启动的时候就设置好了槽位数（Slot），每个 slot 能启动一个 Task，Task 为线程。从 JobManager 处接收需要部署的 Task，部署启动后，与自己的上游建立 Netty 连接，接收数据并处理。

**关于执行图**

Flink 中的执行图可以分成四层：**StreamGraph** -> **JobGraph** -> **ExecutionGraph** -> **物理执行图**。

**StreamGraph**：是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。

**JobGraph**：StreamGraph经过优化后生成了 JobGraph，提交给 JobManager 的数据结构。主要的优化为，将多个符合条件的节点 chain 在一起作为一个节点，这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。

**ExecutionGraph**：JobManager 根据 JobGraph 生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本，是调度层最核心的数据结构。

**物理执行图**：JobManager 根据 ExecutionGraph 对 Job 进行调度后，在各个TaskManager 上部署 Task 后形成的“图”，并不是一个具体的数据结构。

## 3 Worker与Slots

**每一个worker(TaskManager)****是一个JVM****进程，它可能会在独立的线程上执行一个或多个subtask**。为了控制一个worker能接收多少个task，worker通过task slot来进行控制（一个worker至少有一个task slot）。·

每个task slot表示TaskManager拥有资源的一个固定大小的子集。假如一个TaskManager有三个slot，那么它会将其管理的内存分成三份给各个slot。**资源****slot****化意味着一个subtask****将不需要跟来自其他job****的subtask****竞争被管理的内存，取而代之的是它将拥有一定数量的内存储备**。需要注意的是，这里不会涉及到CPU的隔离，slot目前仅仅用来隔离task的受管理的内存。

**通过调整task slot****的数量，允许用户定义subtask****之间如何互相隔离**。如果一个TaskManager一个slot，那将意味着每个task group运行在独立的JVM中（该JVM可能是通过一个特定的容器启动的），而一个TaskManager多个slot意味着更多的subtask可以共享同一个JVM。而在同一个JVM进程中的task将共享TCP连接（基于多路复用）和心跳消息。它们也可能共享数据集和数据结构，因此这减少了每个task的负载。

 图 TaskManager与Slot

**Task Slot****是静态的概念，是指TaskManager****具有的并发执行能力**，可以通过参数taskmanager.numberOfTaskSlots进行配置，而**并行度****parallelism****是动态概念，即TaskManager****运行程序时实际使用的并发能力**，可以通过参数parallelism.default进行配置。

也就是说，假设一共有3个TaskManager，每一个TaskManager中的分配3个TaskSlot，也就是每个TaskManager可以接收3个task，一共9个TaskSlot，如果我们设置parallelism.default=1，即运行程序默认的并行度为1，9个TaskSlot只用了1个，有8个空闲，因此，设置合适的并行度才能提高效率。

## 4 并行数据流

**Flink****程序的执行具有并行、分布式的特性**。在执行过程中，一个 stream 包含一个或多个 stream partition ，而每一个 operator 包含一个或多个 operator subtask，这些operator subtasks在不同的线程、不同的物理机或不同的容器中彼此互不依赖得执行。

**一个特定operator****的subtask****的个数被称之为其parallelism(****并行度)**。一个stream的并行度总是等同于其producing operator的并行度。一个程序中，不同的operator可能具有不同的并行度。

图 并行数据流

Stream在operator之间传输数据的形式可以是one-to-one(forwarding)的模式也可以是redistributing的模式，具体是哪一种形式，取决于operator的种类。

**One-to-one**：**stream(****比如在source****和map operator****之间)****维护着分区以及元素的顺序**。那意味着map operator的subtask看到的元素的个数以及顺序跟source operator的subtask生产的元素的个数、顺序相同，map、fliter、flatMap等算子都是one-to-one的对应关系。

 **类似于spark****中的窄依赖**

**Redistributing**：**stream(map()****跟keyBy/window****之间或者keyBy/window****跟sink****之间)****的分区会发生改变**。每一个operator subtask依据所选择的transformation发送数据到不同的目标subtask。例如，keyBy() 基于hashCode重分区、broadcast和rebalance会随机重新分区，这些算子都会引起redistribute过程，而redistribute过程就类似于Spark中的shuffle过程。

**类似于spark****中的宽依赖**

##  Flink中的数据类型和处理模型

Flink中的数据类型有有界数据流和无界数据流，所谓有界数据流指的是有边界的数据，比如批次数据、流式数据中的窗口数据。无界数据流指的是没有边界的数据流，比如源源不断产生的流式数据。

Flink中三种处理数据模型：

l Flink批处理

Flink批处理中处理的有界数据流

l Flin流式处理

Flink流式处理中处理的有有界数据流也有无界数据流

l FlinSQL处理

FlinkSQL中处理的有有界数据流也有无界数据流

## Flink Source和Sink

l 数据源Source

Source 是Flink获取数据的地方。以下source中和批处理的source类似，但是以下源作为dataStream流处理时，是一条条处理，最终得到的不是一个总结果，而是每次处理后都会得到一个结果。

1) socketTextStream – 读取Socket数据流

2) readTextFile() -- 逐行读取文本文件获取数据流，每行都返回字符串。

3) fromCollection() – 从集合中创建数据流。

4) fromElements – 从给定的数据对象创建数据流，所有数据类型要一致。

5) addSource – 添加新的源函数,例如从kafka中读取数据，参见读取kafka数据案例。

l 数据写出 Sink

1) writeAsText() – 以字符串的形式逐行写入文件，调用每个元素的toString()得到写入的字符串。

2) writeAsCsv() – 将元组写出以逗号分隔的csv文件。注意：只能作用到元组数据上。

3) print() – 控制台直接输出结果，调用对象的toString()方法得到输出结果。

addSink() – 自定义接收函数。例如将结果保存到kafka中，参见kafka案例。

## Flink术语

 Dataflows:

l parallel Dataflows:

l Task 和算子链 :

## Flink角色

JobManager、TaskManager和clients:

Flink运行时包含两种类型的进程：

JobManger:也叫作masters,协调分布式执行，调度task,协调checkpoint，协调故障恢复。在Flink程序中至少有一个JobManager,高可用可以设置多个JobManager,其中一个是Leader,其他都是standby状态。

TaskManager:也叫workers,执行dataflow生成的task,负责缓冲数据，及TaskManager之间的交换数据。Flink程序中必须有一个TaskManager.

Flink程序可以运行在standalone集群，Yarn或者Mesos资源调度框架中。

clients不是Flink程序运行时的一部分，作用是向JobManager准备和发送dataflow,之后，客户端可以断开连接或者保持连接。

<file_sep>/notes/Phoenix.md
# Phoenix简介

## 1.1 Phoenix定义

Phoenix是HBase的开源SQL皮肤。可以使用标准JDBC API代替HBase客户端API来创建表，插入数据和查询HBase数据。

## 1.2 Phoenix特点

1) 容易集成：如Spark，Hive，Pig，Flume和Map Reduce。

2) 性能好：直接使用HBase API以及协处理器和自定义过滤器，可以为小型查询提供毫秒级的[性能](http://phoenix.apache.org/performance.html)，或者为数千万行提供数秒的性能。

3) 操作简单：DML命令以及通过DDL命令创建表和版本化增量更改。

4) 安全功能: [支持GRANT和REVOKE](https://issues.apache.org/jira/browse/PHOENIX-672) 。

5) 完美支持Hbase二级索引创建。

## Phoenix和Hive的区别

Hive是HDFS的SQL层
Phoenix是HBase的SQL层

对象不一样
使用场景区分很明显

Hive感觉更适合离线资料分析
并且无效能要求那种

Phoenix由于是搭载HBase上
所以比较适合一些real time的
当然对离线资料也是可以的

## Pheonix操作

## shell操作

### 表的操作

put ,insert,update,delete,create,select之类的,用到再查

**Hbase对表的操作**

![image-20200425221708610](pictures/Phoenix/image-20200425221708610.png)

**Phoenix对表的操作,也支持复杂join,比如 grouped joins(或子查询)，以及derived-tables（派生表）的join操作。**

![image-20200425222441697](pictures/Phoenix/image-20200425222441697.png)

![image-20200426090007632](pictures/Phoenix/image-20200426090007632.png)

![image-20200426090309775](pictures/Phoenix/image-20200426090309775.png)

### 表的映射

当hbase中不存在表的时候,用pheonix的命令创建的时候,会自动关联hbase和pheonix

当hbase中存在表的时候,可以用创建视图的方式关联表,但视图只能查询,不能删改原来的数据表的信息.但视图可以删改.

![image-20200426090353051](pictures/Phoenix/image-20200426090353051.png)

# Pheonix二级索引

一级索引是rowKey,Hbase里面有很多对于一级索引的扫描加速,比如布隆过滤器.

## 协处理器源码简单理解

postPut的意思是在put操作之后触发.e里面存放表信息,put里面存放put进去的数据

![image-20200425200221178](pictures/Phoenix/image-20200425200221178.png)

## Hbase里面配置协处理器

网上找博客就行,就是在hbase的启动文件里面配置

## Hbase创建二级索引

### phoenix的索引分类

#### 1）global index

是默认的索引格式。适用于多读少写的业务场景。写数据的时候会消耗大量开销，因为索引表也要更新，而索引表是分布在不同的数据节点上的，跨节点的数据传输带来了较大的性能消耗。在读数据的时候Phoenix会选择索引表来降低查询消耗的时间。如果想查询的字段不是索引字段的话索引表不会被使用，也就是说不会带来查询速度的提升。

CREATE INDEX stu_index_name ON stu (name)

#### 建立单属性索引表的内容:

![image-20200425220448446](pictures/Phoenix/image-20200425220448446.png)

但是,select name,sex from stu where name = "sx";是fullscan

![image-20200425232943288](pictures/Phoenix/image-20200425232943288.png)

因为select还需要sex字段.解决的办法是创建多列索引:CREATE INDEX stu_index_name_sex ON stu  (name) INCLUDE (sex);

![image-20200425232958799](pictures/Phoenix/image-20200425232958799.png)

此时,索引表里面的结构是:rowkey用name+rowId来表示  value里面存放性别.

此时再用explain来分析,发现已经走索引了,不是full scan 而是range scan了.

#### 建立多属性索引表的内容:

CREATE INDEX stu_index_name ON stu (name) **INCLUDE (sex)**

查看索引表内容发现,性别那一列放在了column里面

![image-20200425220654969](pictures/Phoenix/image-20200425220654969.png)

#### 全局索引的覆盖细节

假设有列 rowKey name sex old ;有索引 name,sex;

只有当select后面的属性 和 where后面的属性同时被索引表包含的时候,才能走索引.

select rowKey from stu where rowKey = 1;  走索引,因为rowkey是一级索引

select name,sex from stu where name = "sx";走索引 因为 select后面的属性 和 where后面的属性同时被索引表的属性覆盖

select name,sex from stu where rowkey = 1; 走一级索引

select name,old from stu where name = "sx";**不走索引,因为select name,old 里面 old没有被索引表覆盖**

select name,sex from stu where sex = "male";**不走索引,因为要二级索引中,name在前,sex在后**

select name,sex from stu where rowKey = 1;**走索引,因为要一级索引里面有涵盖所有属性的索引**

#### 全局索引表的扫瞄细节

只要 select后面的属性 和 where后面的属性同时被索引表的属性覆盖,那么只需要走索引表就可以

#### 2）Local index

适用于写操作频繁的场景。索引数据和数据表的数据是存放在相同的服务器中的，避免了在写操作的时候往不同服务器的索引表中写索引带来的额外开销。查询的字段不是索引字段索引表也会被使用，这会带来查询速度的提升。

CREATE LOCAL INDEX my_index ON my_table (my_index)

#### 本地索引示例

首先创建表,分了_ a b c 四个分区,然后插入数据:

![image-20200425224146789](pictures/Phoenix/image-20200425224146789.png)

然后创建索引

![image-20200425224104446](pictures/Phoenix/image-20200425224104446.png)

浏览索引:

```
\x00是下划线_的意思,所以下图的ROW代表的是下面这样:

___zhangsan_1001(_第一个分区)
1001
1001
1001

b__lisi_b_102(b分区)
b_102
b_102
b_102

```

![image-20200425223635273](pictures/Phoenix/image-20200425223635273.png)

#### 局部索引的扫瞄次数

像select name,sex from stu where name = "zhangsan"的扫瞄分为两次

第一次: 扫瞄索引数据`(  ___zhangsan_1001   b__lisi_b_102 )`找到rowkey (`b_102`)

第二次:扫瞄三个`b_102`找到符合条件的col来返回

### 三种提升效率查询方式

| 1） CREATE INDEX my_index ON my_table (v1) INCLUDE (v2) |
| ------------------------------------------------------- |
| 2） SELECT v2 FROM my_table WHERE v1 = 'foo'            |
| 3） CREATE LOCAL INDEX my_index ON my_table (v1)        |

### 如何删除索引

DROP INDEX my_index ON my_table

### 二级索引的协处理器死循环的情况

![image-20200426082233163](pictures/Phoenix/image-20200426082233163.png)

在put进stu1之后触发,再往stu1里面插入一条数据.会导致死循环.解决办法是用一个flag来标记后一条put数据来自协处理器.

**解决办法**

Hbase需要重启,重启前把协处理器关掉,同时把stu1这张表删除掉.

配置修改处1:

![image-20200426083320666](pictures/Phoenix/image-20200426083320666.png)

配置修改处2:

![image-20200426083638912](pictures/Phoenix/image-20200426083638912.png)

### truncate的坑

trucate不只是会删除表里面的数据,还会删除表的元数据,比如原来分区3个,删除得只剩了默认一个分区.

但无法删除协处理器.

![image-20200426085011420](pictures/Phoenix/image-20200426085011420.png)

<file_sep>/notes/设计模式.md

- [1. 单例（Singleton）](#1-%e5%8d%95%e4%be%8bsingleton)
  - [Intent](#intent)
  - [Class Diagram](#class-diagram)
  - [Implementation](#implementation)
    - [Ⅰ 懒汉式-线程不安全](#%e2%85%a0-%e6%87%92%e6%b1%89%e5%bc%8f-%e7%ba%bf%e7%a8%8b%e4%b8%8d%e5%ae%89%e5%85%a8)
    - [Ⅱ 饿汉式-线程安全](#%e2%85%a1-%e9%a5%bf%e6%b1%89%e5%bc%8f-%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8)
    - [Ⅲ 懒汉式-线程安全](#%e2%85%a2-%e6%87%92%e6%b1%89%e5%bc%8f-%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8)
    - [Ⅳ 双重校验锁-线程安全](#%e2%85%a3-%e5%8f%8c%e9%87%8d%e6%a0%a1%e9%aa%8c%e9%94%81-%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8)
    - [Ⅴ 静态内部类实现](#%e2%85%a4-%e9%9d%99%e6%80%81%e5%86%85%e9%83%a8%e7%b1%bb%e5%ae%9e%e7%8e%b0)
    - [Ⅵ 枚举实现](#%e2%85%a5-%e6%9e%9a%e4%b8%be%e5%ae%9e%e7%8e%b0)
  - [Examples](#examples)
  - [JDK](#jdk)

## 1. 单例（Singleton）

### Intent

确保一个类只有一个实例，并提供该实例的全局访问点。

### Class Diagram

使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。

私有构造函数保证了不能通过构造函数来创建对象实例，只能通过公有静态函数返回唯一的私有静态变量。

### Implementation

```java
public class Solution {

    private static Solution uniqueSolution = null;
//    private static Solution uniqueSolution = new Solution();

    private Solution() {

    }

//懒汉线程不安全
//    public static Solution getUniqueSolution() {
//        if (uniqueSolution == null) {
//            uniqueSolution = new Solution();
//        }
//        return uniqueSolution;
//    }
//饿汉 线程安全 浪费内存资源
    //    public static Solution getUniqueSolution() {
//        return uniqueSolution;
//    }

    //懒汉线程安全-浪费等待时间
//    public static synchronized Solution getUniqueSolution() {
//        if (uniqueSolution == null) {
//            uniqueSolution = new Solution();
//        }
//        return uniqueSolution;
//    }

    //懒汉双重校验锁
    private volatile static Singleton uniqueInstance;
    public static Solution getUniqueSolution() {
        if (uniqueSolution == null) {
            synchronized (Solution.class) {
                if (uniqueSolution == null) {
                    uniqueSolution = new Solution();
                }
            }
        }
        return uniqueSolution;
    }
```

#### Ⅰ 懒汉式-线程不安全

以下实现中，私有静态变量 uniqueInstance 被延迟实例化，这样做的好处是，如果没有用到该类，那么就不会实例化 uniqueInstance，从而节约资源。

这个实现在多线程环境下是不安全的，如果多个线程能够同时进入 `if (uniqueInstance == null)` ，并且此时 uniqueInstance 为 null，那么会有多个线程执行 `uniqueInstance = new Singleton();` 语句，这将导致实例化多次 uniqueInstance。

```java
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
```

#### Ⅱ 饿汉式-线程安全

线程不安全问题主要是由于 uniqueInstance 被实例化多次，采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。

但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。

```java
private static Singleton uniqueInstance = new Singleton();
```

#### Ⅲ 懒汉式-线程安全

只需要对 getUniqueInstance() 方法加锁，那么在一个时间点只能有一个线程能够进入该方法，从而避免了实例化多次 uniqueInstance。

但是当一个线程进入该方法之后，其它试图进入该方法的线程都必须等待，即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过长，因此该方法有性能问题，不推荐使用。

```java
public static synchronized Singleton getUniqueInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}
```

#### Ⅳ 双重校验锁-线程安全

uniqueInstance 只需要被实例化一次，之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行，只有当 uniqueInstance 没有被实例化时，才需要进行加锁。

双重校验锁先判断 uniqueInstance 是否已经被实例化，如果没有被实例化，那么才对实例化语句进行加锁。

```java
public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
```

考虑下面的实现，也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下，如果两个线程都执行了 if 语句，那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作，但是两个线程都会执行 `uniqueInstance = new Singleton();` 这条语句，只是先后的问题，那么就会进行两次实例化。因此必须使用双重校验锁，也就是需要使用两个 if 语句：第一个 if 语句用来避免 uniqueInstance 已经被实例化之后的加锁操作，而第二个 if 语句进行了加锁，所以只能有一个线程进入，就不会出现 uniqueInstance == null 时两个线程同时进行实例化操作。

```java
if (uniqueInstance == null) {
    synchronized (Singleton.class) {
        uniqueInstance = new Singleton();
    }
}
```

uniqueInstance 采用 volatile 关键字修饰也是很有必要的， `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行：

1. 为 uniqueInstance 分配内存空间
2. 初始化 uniqueInstance
3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性，执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题，但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如，线程 T1 执行了 1 和 3，此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空，因此返回 uniqueInstance，但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排，保证在多线程环境下也能正常运行。

#### Ⅴ 静态内部类实现

当 Singleton 类被加载时，静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance()` 方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载，此时初始化 INSTANCE 实例，并且 JVM 能确保 INSTANCE 只被实例化一次。

这种方式不仅具有延迟初始化的好处，而且由 JVM 提供了对线程安全的支持。

```java
public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}
```

#### Ⅵ 枚举实现

```java
public enum Singleton {

    INSTANCE;

    private String objName;

    public String getObjName() {
        return objName;
    }

    public void setObjName(String objName) {
        this.objName = objName;
    }

    public static void main(String[] args) {

        // 单例测试
        Singleton firstSingleton = Singleton.INSTANCE;
        firstSingleton.setObjName("firstName");
        System.out.println(firstSingleton.getObjName());
        Singleton secondSingleton = Singleton.INSTANCE;
        secondSingleton.setObjName("secondName");
        System.out.println(firstSingleton.getObjName());
        System.out.println(secondSingleton.getObjName());

        // 反射获取实例测试
        try {
            Singleton[] enumConstants = Singleton.class.getEnumConstants();
            for (Singleton enumConstant : enumConstants) {
                System.out.println(enumConstant.getObjName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

```html
firstName
secondName
secondName
secondName
```

该实现可以防止反射攻击。在其它实现中，通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public，然后调用构造函数从而实例化对象，如果要防止这种攻击，需要在构造函数中添加防止多次实例化的代码。该实现是由 JVM 保证只会实例化一次，因此不会出现上述的反射攻击。

该实现在多次序列化和序列化之后，不会得到多个实例。而其它实现需要使用 transient 修饰所有字段，并且实现序列化和反序列化的方法。

### Examples

- Logger Classes
- Configuration Classes
- Accesing resources in shared mode
- Factories implemented as Singletons

### JDK

- [java.lang.Runtime#getRuntime()](http://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#getRuntime%28%29)
- [java.awt.Desktop#getDesktop()](http://docs.oracle.com/javase/8/docs/api/java/awt/Desktop.html#getDesktop--)
- [java.lang.System#getSecurityManager()](http://docs.oracle.com/javase/8/docs/api/java/lang/System.html#getSecurityManager--)<file_sep>/notes/MYSQL.md

- [索引](#索引)
    - [B+ Tree 原理](#b-tree-原理)
        - [1. 数据结构](#1-数据结构)
        - [2. 操作](#2-操作)
        - [3. 与红黑树的比较](#3-与红黑树的比较)
        - [红黑树介绍](#红黑树介绍)
- [树的介绍](#树的介绍)
    - [普通二叉查找树](#普通二叉查找树)
    - [B树与B+树](#b树与b树)
        - [B树出现的原因](#b树出现的原因)
        - [B+树的特点](#b树的特点)
        - [B+树比B树的优点](#b树比b树的优点)
    - [MySQL 索引](#mysql-索引)
        - [1. B+Tree 索引](#1-btree-索引)
        - [2. 哈希索引](#2-哈希索引)
        - [3. 全文索引](#3-全文索引)
        - [倒排索引:](#倒排索引)
        - [4. 空间数据索引](#4-空间数据索引)
    - [索引优化](#索引优化)
        - [1. 独立的列](#1-独立的列)
        - [2. 多列索引](#2-多列索引)
        - [3. 索引列的顺序](#3-索引列的顺序)
        - [4. 前缀索引](#4-前缀索引)
        - [5. 覆盖索引](#5-覆盖索引)
    - [聚集索引,辅助索引,组合索引,非聚集索引](#聚集索引辅助索引组合索引非聚集索引)
    - [索引的优点](#索引的优点)
    - [索引的使用条件](#索引的使用条件)
- [索引失效的情况](#索引失效的情况)
    - [列与列对比](#列与列对比)
    - [存在NULL值条件](#存在null值条件)
    - [NOT条件](#not条件)
    - [LIKE通配符](#like通配符)
    - [条件上包括函数](#条件上包括函数)
    - [复合索引前导列区分大](#复合索引前导列区分大)
    - [数据类型的转换](#数据类型的转换)
    - [谓词运算](#谓词运算)
    - [Vistual Index](#vistual-index)
    - [Invisible Index](#invisible-index)
- [二、查询性能优化](#二查询性能优化)
    - [使用 Explain 进行分析](#使用-explain-进行分析)
    - [优化数据访问](#优化数据访问)
        - [1. 减少请求的数据量](#1-减少请求的数据量)
        - [2. 减少服务器端扫描的行数](#2-减少服务器端扫描的行数)
    - [重构查询方式](#重构查询方式)
        - [1. 切分大查询](#1-切分大查询)
        - [2. 分解大连接查询](#2-分解大连接查询)
- [三、存储引擎](#三存储引擎)
    - [InnoDB](#innodb)
    - [MyISAM](#myisam)
    - [比较](#比较)
    - [隔离级别](#隔离级别)
- [四、数据类型](#四数据类型)
    - [整型](#整型)
    - [浮点数](#浮点数)
    - [字符串](#字符串)
    - [时间和日期](#时间和日期)
        - [1. DATETIME](#1-datetime)
        - [2. TIMESTAMP](#2-timestamp)
- [五、切分](#五切分)
    - [分库分表中间件](#分库分表中间件)
    - [水平切分](#水平切分)
    - [垂直切分](#垂直切分)
    - [Sharding 策略](#sharding-策略)
        - [顺序分库分表](#顺序分库分表)
        - [取模分库分表](#取模分库分表)
        - [映射表：使用单独的一个数据库来存储映射关系。](#映射表使用单独的一个数据库来存储映射关系)
    - [Sharding 存在的问题](#sharding-存在的问题)
        - [1. 事务问题](#1-事务问题)
        - [2. 连接](#2-连接)
        - [3. ID 唯一性](#3-id-唯一性)
        - [跨库join](#跨库join)
        - [跨节点的count,order by,group by以及聚合函数问题](#跨节点的countorder-bygroup-by以及聚合函数问题)
        - [数据迁移，容量规划，扩容等问题](#数据迁移容量规划扩容等问题)
- [六、复制](#六复制)
    - [主从复制](#主从复制)
    - [读写分离](#读写分离)
- [事务隔离机制](#事务隔离机制)
    - [事务必须满足ACID](#事务必须满足acid)
    - [事务的定义](#事务的定义)
    - [并发事务会出现的问题](#并发事务会出现的问题)
        - [第一类丢失更新](#第一类丢失更新)
        - [第二类丢失更新](#第二类丢失更新)
        - [脏读：](#脏读)
        - [不可重复读：](#不可重复读)
        - [幻读:](#幻读)
    - [四种事务隔离机制](#四种事务隔离机制)
        - [读未提交](#读未提交)
        - [读已提交](#读已提交)
        - [Repeatable read 可重复读](#repeatable-read-可重复读)
        - [Serializable 串行化](#serializable-串行化)
    - [事务隔离机制的实现原理](#事务隔离机制的实现原理)
    - [MVCC](#mvcc)
        - [定义](#定义)
        - [实现](#实现)
            - [insert](#insert)
            - [select](#select)
            - [delete](#delete)
            - [update](#update)
        - [mvcc解决幻读](#mvcc解决幻读)
    - [数据库的锁](#数据库的锁)
        - [乐观锁与悲观锁](#乐观锁与悲观锁)
        - [共享锁(读锁,S锁)](#共享锁读锁s锁)
        - [排他锁(写锁,X锁)](#排他锁写锁x锁)
        - [更新锁(意向锁)](#更新锁意向锁)
        - [锁粒度](#锁粒度)
- [解决超卖](#解决超卖)
    - [方法1：](#方法1)
    - [方法二：](#方法二)

# 索引

## B+ Tree 原理

### 1. 数据结构

B Tree 指的是 Balance Tree，也就是平衡树。平衡树是一颗查找树，并且所有叶子节点位于同一层。

B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现，它具有 B Tree 的平衡性，并且通过顺序访问指针来提高区间查询的性能。

在 B+ Tree 中，一个节点中的 key 从左到右非递减排列，如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1，且不为 null，则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。

### 2. 操作

进行查找操作时，首先在根节点进行二分查找，找到一个 key 所在的指针，然后递归地在指针所指向的节点进行查找。直到查找到叶子节点，然后在叶子节点上进行二分查找，找出 key 所对应的 data。

插入删除操作会破坏平衡树的平衡性，因此在插入删除操作之后，需要对树进行一个分裂、合并、旋转等操作来维护平衡性。

### 3. 与红黑树的比较

红黑树等平衡树也可以用来实现索引，但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构，主要有以下两个原因：

（一）更少的查找次数

平衡树查找操作的时间复杂度和树高 h 相关，O(h)=O(logdN)，其中 d 为每个节点的出度。

红黑树的出度为 2，而 B+ Tree 的出度一般都非常大，所以红黑树的树高 h 很明显比 B+ Tree 大非常多，查找的次数也就更多。

（二）利用磁盘预读特性

为了减少磁盘 I/O 操作，磁盘往往不是严格按需读取，而是每次都会预读。预读过程中，磁盘进行顺序读取，顺序读取不需要进行磁盘寻道，并且只需要很短的磁盘旋转时间，速度会非常快。

操作系统一般将内存和磁盘分割成固定大小的块，每一块称为一页，内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小，使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性，相邻的节点也能够被预先载入。
### 红黑树介绍
红黑树是一棵二叉搜索树，它在每个节点增加了一个存储位记录节点的颜色，可以是RED,也可以是BLACK；通过任意一条从根到叶子简单路径上颜色的约束，红黑树保证最长路径不超过最短路径的二倍，因而近似平衡。

具体性质如下：

每个节点颜色不是黑色，就是红色
根节点是黑色的
如果一个节点是红色，那么它的两个子节点就是黑色的（没有连续的红节点）
对于每个节点，从该节点到其后代叶节点的简单路径上，均包含相同数目的黑色节点

满足以上性质时，就能保证最长路径不超过最短路径的二倍,因为你的最短路径就是全黑节点，最长路径就是一个红节点一个黑节点，最后黑色节点相同时，最长路径刚好是最短路径的两倍

[红黑树参考博客](https://blog.csdn.net/tanrui519521/article/details/80980135)

# 树的介绍
## 普通二叉查找树

非平衡,查找的时间复杂度是Ｏ(log N)

## B树与B+树

### B树出现的原因
B-树就是B树，没有所谓的B减树

 树就是常说的“B 减树（B- 树）”，又名平衡多路（即不止两个子树）查找树，它和平衡二叉树的不同有这么几点：

1. 平衡二叉树节点最多有两个子树，而 B 树每个节点可以有多个子树，**M 阶 B 树表示该树每个节点最多有 M 个子树**
2. 平衡二叉树每个节点只有一个数据和两个指向孩子的指针，而 B 树每个**中间节点**有 k-1 个关键字（可以理解为数据）和 k 个子树（ **k 介于阶数 M 和 M/2 之间，M/2 向上取整）
3. B 树的所有叶子节点都在同一层，并且叶子节点只有关键字，指向孩子的指针为 null

和平衡二叉树相同的点在于：B 树的节点数据大小也是按照左小右大，子树与节点的大小比较决定了子树指针所处位置。

![image-20200306095950678](pictures/MYSQL/image-20200306095950678.png)

### B+树的特点

1.有k个子树的中间节点包含有k个元素（B树中是k-1个元素），**每个元素不保存数据，只用来索引，所有数据都保存在叶子节点。**

2.所有的叶子结点中包含了全部元素的信息，及指向含这些元素记录的指针，且叶子结点本身依关键字的大小自小而大顺序链接。-

3.所有的**中间节点元素都同时存在于子节点，在子节点元素中是最大（或最小）元素。**

4.**每个叶子结点都存有相邻叶子结点的指针，叶子结点本身依关键字的大小自小而大顺序链接**

### B+树比B树的优点
1.**多阶**:单一节点存储更多的元素，使得时间复杂度的logN的底数更大,树更矮,io次数变少.

2.**层数一致**,关键字查询路径长度相同,查询性能稳定；

3.**只有叶子节点**存数据,一次性读取进内存的关键字也会更多;

4.叶子节点**顺序存储数据**:可以利用磁盘的顺序IO,减少IO读取次数,读取速度更快;

5.**叶子节点形成有序链表**:便于pre和last的范围查询。

## MySQL 索引

索引是在存储引擎层实现的，而不是在服务器层实现的，所以不同存储引擎具有不同的索引类型和实现。

### 1. B+Tree 索引

是大多数 MySQL 存储引擎的默认索引类型。

因为不再需要进行全表扫描，只需要对树进行搜索即可，所以查找速度快很多。

因为 B+ Tree 的有序性，所以除了用于查找，还可以用于排序和分组。

可以指定多个列作为索引列，多个索引列共同组成键。

适用于全键值、键值范围和键前缀查找，其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找，则无法使用索引。

InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录，这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方，所以一个表只能有一个聚簇索引。

辅助索引的叶子节点的 data 域记录着主键的值，因此在使用辅助索引进行查找时，需要先在辅助索引中查找到主键值，然后再到主索引中进行查找。

### 2. 哈希索引

哈希索引能以 O(1) 时间进行查找，但是失去了有序性：

- 无法用于排序与分组；
- 只支持精确查找，无法用于部分查找和范围查找。

InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”，当某个索引值被使用的非常频繁时，会在 B+Tree 索引之上再创建一个哈希索引，这样就让 B+Tree 索引具有哈希索引的一些优点，比如快速的哈希查找。

### 3. 全文索引

**MyISAM** 存储引擎支持全文索引，用于查找文本中的关键词，而不是直接比较是否相等。

查找条件使用 MATCH AGAINST，而不是普通的 WHERE。

全文索引使用倒排索引实现，它记录着关键词到其所在文档的映射。

InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。

### 倒排索引:

倒排索引(反向索引) 与 正向索引对应,正向索引是以文档id为主键,文档里面有哪些关键单词为内容,如图:

而反向索引是以word为主键,word所在的多个docId为关键内容的索引,如图:

**实例如下**

关键单词作为索引,倒排列表中记录该单词在哪些文档中出现以及出现的位置在哪

### 4. 空间数据索引

MyISAM 存储引擎支持空间数据索引（R-Tree），可以用于地理数据存储。空间数据索引会从所有维度来索引数据，可以有效地使用任意维度来进行组合查询。

必须使用 GIS 相关的函数来维护数据。

## 索引优化

### 1. 独立的列

在进行查询时，索引列不能是表达式的一部分，也不能是函数的参数，否则无法使用索引。

例如下面的查询不能使用 actor_id 列的索引：

```sql
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
```

### 2. 多列索引

在需要使用多个列作为条件进行查询时，使用多列索引比使用多个单列索引性能更好。例如下面的语句中，最好把 actor_id 和 film_id 设置为多列索引。

```sql
SELECT film_id, actor_ id FROM sakila.film_actor
WHERE actor_id = 1 AND film_id = 1;
```

### 3. 索引列的顺序

让选择性最强的索引列放在前面。

索引的选择性是指：不重复的索引值和记录总数的比值。最大值为 1，此时每个记录都有唯一的索引与其对应。选择性越高，每个记录的区分度越高，查询效率也越高。

例如下面显示的结果中 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
```

### 4. 前缀索引

对于 BLOB、TEXT 和 VARCHAR 类型的列，必须使用前缀索引，只索引开始的部分字符。

前缀长度的选取需要根据索引选择性来确定
索引选择性指的是distinct的个数与总数的比值,最大为1,如果选取合适prefix_length就可以保证很接近索引选择性的话,用前一段做索引就可以节省空间

实例:

### 5. 覆盖索引

覆盖索引是一种辅助索引,辅助索引包含所有需要查询的字段的值,只需要访问这个覆盖索引就可以得到要查询的字段的值,而不需要再访问主键索引(聚集索引)拿到一整行的值.

具有以下优点：

- 索引通常远小于数据行的大小,因为它只是由一行的部分字段组成的索引不包含一整行数据，只读取索引能大大减少数据访问量。
- 一些存储引擎（例如 MyISAM）在内存中只缓存索引，而数据依赖于操作系统来缓存。因此，只访问索引可以不使用系统调用（通常比较费时）。
- 对于 InnoDB 引擎，若辅助索引能够覆盖查询，则无需访问主索引。

## 聚集索引,辅助索引,组合索引,非聚集索引

聚集索引与非聚集索引
聚集索引:
一张表的主键存储在B+树中,同时叶子节点存放表的每一行的数据,叶子节点存储在数据页中(方便磁盘顺序读取,这也是B+树优于红黑树的一大原因)
非聚集索引
叶子节点存储的不是数据,而是指向某个数据块的指针.所以数据块不是按照顺序存放的

聚集索引和辅助索引

聚集索引（主键索引）
—innodb存储引擎是索引组织表，即表中的数据按照主键顺序存放。而聚集索引就是按照每张表的主键构造一颗B+树，同时叶子节点中存放的即为整张表的记录数据
—聚集索引的叶子节点称为数据页，数据页，数据页！重要的事说三遍。聚集索引的这个特性决定了索引组织表中的数据也是索引的一部分。

辅助索引（二级索引）
—非主键索引
—叶子节点=键值+书签。Innodb存储引擎的书签就是相应行数据的主键索引值

单列索引与组合索引(复合索引,联合索引)
单列索引，顾名思义也就是只有一个字段的索引列。
组合索引，又称复合索引，两个或更多个列上的索引被称作复合索引。对于复合索引，他们都遵循左侧原则，也是就是说一个查询可以只使用复合索引最左侧的一部份。例如索引是key index (a,b,c). 可以支持a | a,b| a,b,c 3种组合进行查找，但不支持 b,c进行查找 .当最左侧字段是常量引用时，索引就十分有效。

## 索引的优点

- 大大减少了服务器需要扫描的数据行数。

- 帮助服务器避免进行排序和分组，以及避免创建临时表（B+Tree 索引是有序的，可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建，不需要排序和分组，也就不需要创建临时表）。

- 将随机 I/O 变为顺序 I/O（B+Tree 索引是有序的，会将相邻的数据都存储在一起）。

## 索引的使用条件

- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效；

- 对于中到大型的表，索引就非常有效；

- 但是对于特大型的表，建立和维护索引的代价将会随之增长。这种情况下，需要用到一种技术可以直接区分出需要查询的一组数据，而不是一条记录一条记录地匹配，例如可以使用分区技术。

# 索引失效的情况

虽然你这列上建了索引，查询条件也是索引列，但最终执行计划没有走它的索引。下面是引起这种问题的几个关键点。

## 列与列对比

某个表中，有两列（id和c_id）都建了单独索引，下面这种查询条件不会走索引

select * from test where id=c_id;

这种情况会被认为还不如走全表扫描。

## 存在NULL值条件

我们在设计数据库表时，应该尽力避免NULL值出现，如果非要不可避免的要出现NULL值，也要给一个DEFAULT值，数值型可以给0、-1之类的， 字符串有时候给空串有问题，就给一个空格或其他。如果索引列是可空的，是不会给其建索引的，索引值是少于表的count(*)值的，所以这种情况下，执行计划自然就去扫描全表了。

select * from test where id is not null;

## NOT条件

我们知道建立索引时，给每一个索引列建立一个条目，如果查询条件为等值或范围查询时，索引可以根据查询条件去找对应的条目。反过来当查询条件为非时，索引定位就困难了，执行计划此时可能更倾向于全表扫描，这类的查询条件有：<>、NOT、in、not exists

select * from test where id<>500;
select * from test where id in (1,2,3,4,5);
select * from test where not in (6,7,8,9,0);
select * from test where not exists (select 1 from test_02 where test_02.id=test.id);

## LIKE通配符

当使用模糊搜索时，尽量采用后置的通配符，例如：name||’%’，因为走索引时，其会从前去匹配索引列，这时候是可以找到的，如果采用前匹配，那么查索引就会很麻烦，比如查询所有姓张的人，就可以去搜索’张%’。相反如果你查询所有叫‘明’的人，那么只能是%明。这时候索引如何定位呢？前匹配的情况下，执行计划会更倾向于选择全表扫描。后匹配可以走INDEX RANGE SCAN。
所以业务设计的时候，尽量考虑到模糊搜索的问题，要更多的使用后置通配符。

select * from test where name like 张||'%';

## 条件上包括函数

查询条件上尽量不要对索引列使用函数，比如下面这个SQL

select * from test where upper(name)='SUNYANG';

这样是不会走索引的，因为索引在建立时会和计算后可能不同，无法定位到索引。但如果查询条件不是对索引列进行计算，那么依然可以走索引。比如

select * from test where name=upper('sunyang');
--INDEX RANGE SCAN
这样的函数还有：to_char、to_date、to_number、trunc等

## 复合索引前导列区分大

当复合索引前导列区分小的时候，我们有INDEX SKIP SCAN，当前导列区分度大，且查后导列的时候，前导列的分裂会非常耗资源，执行计划想，还不如全表扫描来的快，然后就索引失效了。

select * from test where owner='sunyang';

## 数据类型的转换

当查询条件存在隐式转换时，索引会失效。比如在数据库里id存的number类型，但是在查询时，却用了下面的形式：

select * from sunyang where id='123';

Connect By Level
使用connect by level时，不会走索引。

## 谓词运算

我们在上面说，不能对索引列进行函数运算，这也包括加减乘除的谓词运算，这也会使索引失效。建立一个sunyang表，索引为id，看这个SQL：

select * from sunyang where id/2=:type_id;

这里很明显对索引列id进行了’/2’除二运算，这时候就会索引失效，这种情况应该改写为：

select * from sunyang where id=:type_id*2;

就可以使用索引了。

## Vistual Index

先说明一下，虚拟索引的建立是否有用，需要看具体的执行计划，如果起作用就可以建一个，如果不起作用就算了。
普通索引这么建：

create index idx_test_id on test(id);

虚拟索引Vistual Index这么建：

create index idx_test_id on test(id) nosegment;

做了一个实验，首先创建一个表：

CREATE TABLE test_1116(
id number,
a number
);

CREATE INDEX idx_test_1116_id on test_1116(id);
CREATE INDEX idx_test_1116_a on test_1116(a)nosegment;

其中id为普通索引，a为虚拟索引。
在表中插入十万条数据

begin
for i in 1 .. 100000 loop
        insert into test_1116 values (i,i);
end loop;
commit;
end;

接着分别去执行下面的SQL看时间，由于在内网机做实验，图贴不出来，数据保证真实性。

select count(id) from test_1116;
--第一次耗时：0.061秒
--第二次耗时：0.016秒

select count(a) from test_1116;
--第一次耗时：0.031秒
--第二次耗时：0.016秒
因为在执行过一次后，oracle对结果集缓存了，所以第二次执行耗时不走索引，走内存就都一样了。
可以看到在这种情况下，虚拟索引比普通索引快了一倍。
具体虚拟索引的使用细节，这里不再展开讨论。

## Invisible Index

Invisible Index是oracle 11g提供的新功能，对优化器（还接到前面博客里讲到的CBO吗）不可见，我感觉这个功能更主要的是测试用，假如一个表上有那么多索引，一个一个去看执行计划调试就很慢了，这时候不如建一个对表和查询都没有影响的Invisible Index来进行调试，就显得很好了。
通过下面的语句来操作索引

alter index idx_test_id invisible;
alter index idx_test_id visible;
如果想让CBO看到Invisible Index，需要加入这句：

alter session set optimizer_use_invisible_indexes = true;

# 二、查询性能优化

## 使用 Explain 进行分析

Explain 用来分析 SELECT 查询语句，开发人员可以通过分析 Explain 结果来优化查询语句。

比较重要的字段有：

- select_type : 查询类型，有简单查询、联合查询、子查询等
- key : 使用的索引
- rows : 扫描的行数

## 优化数据访问

### 1. 减少请求的数据量

- 只返回必要的列：最好不要使用 SELECT * 语句。
- 只返回必要的行：使用 LIMIT 语句来限制返回的数据。
- 缓存重复查询的数据：使用缓存可以避免在数据库中进行查询，特别在要查询的数据经常被重复查询时，缓存带来的查询性能提升将会是非常明显的。

### 2. 减少服务器端扫描的行数

最有效的方式是使用索引来覆盖查询。

## 重构查询方式

### 1. 切分大查询

一个大查询如果一次性执行的话，可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。

```sql
DELETE 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
```

### 2. 分解大连接查询

将一个大连接查询分解成对每一个表进行一次单表查询，然后在应用程序中进行关联，这样做的好处有：

- 让缓存更高效。对于连接查询，如果其中一个表发生变化，那么整个查询缓存就无法使用。而分解后的多个查询，即使其中一个表发生变化，对其它表的查询缓存依然可以使用。
- 分解成多个单表查询，这些单表查询的缓存结果更可能被其它查询使用到，从而减少冗余记录的查询。
- 减少锁竞争；
- 在应用层进行连接，可以更容易对数据库进行拆分，从而更容易做到高性能和可伸缩。
- 查询本身效率也可能会有所提升。例如下面的例子中，使用 IN() 代替连接查询，可以让 MySQL 按照 ID 顺序进行查询，这可能比随机的连接要更高效。

```sql
SELECT * FROM tab
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
```

```sql
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
```

# 三、存储引擎

## InnoDB

是 MySQL 默认的事务型存储引擎，只有在需要它不支持的特性时，才考虑使用其它存储引擎。

实现了四个标准的隔离级别，默认级别是可重复读（REPEATABLE READ）。在可重复读隔离级别下，通过多版本并发控制（MVCC）+ Next-Key Locking 防止幻影读。

主索引是聚簇索引，在索引中保存了数据，从而避免直接读取磁盘，因此对查询性能有很大的提升。

内部做了很多优化，包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。

支持真正的在线热备份。其它存储引擎不支持在线热备份，要获取一致性视图需要停止对所有表的写入，而在读写混合场景中，停止写入可能也意味着停止读取。

## MyISAM

设计简单，数据以紧密格式存储。对于只读数据，或者表比较小、可以容忍修复操作，则依然可以使用它。

提供了大量的特性，包括压缩表、空间数据索引等。

不支持事务。

不支持行级锁，只能对整张表加锁，读取时会对需要读到的所有表加共享锁，写入时则对表加排它锁。但在表有读取操作的同时，也可以往表中插入新的记录，这被称为并发插入（CONCURRENT INSERT）。

可以手工或者自动执行检查和修复操作，但是和事务恢复以及崩溃恢复不同，可能导致一些数据丢失，而且修复操作是非常慢的。

如果指定了 DELAY_KEY_WRITE 选项，在每次修改执行完成时，不会立即将修改的索引数据写入磁盘，而是会写到内存中的键缓冲区，只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能，但是在数据库或者主机崩溃时会造成索引损坏，需要执行修复操作。

## 比较

- 事务：InnoDB 是事务型的，可以使用 Commit 和 Rollback 语句。

- 并发：MyISAM 只支持表级锁，而 InnoDB 还支持行级锁。

- 外键：InnoDB 支持外键。

- 备份：InnoDB 支持在线热备份。

- 崩溃恢复：MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多，而且恢复的速度也更慢。

- 其它特性：MyISAM 支持压缩表和空间数据索引。
## 隔离级别

# 四、数据类型

## 整型

TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间，一般情况下越小的列越好。

INT(11) 中的数字只是规定了交互工具显示字符的个数，对于存储和计算来说是没有意义的。

## 浮点数

FLOAT 和 DOUBLE 为浮点类型，DECIMAL 为高精度小数类型。CPU 原生支持浮点运算，但是不支持 DECIMAl 类型的计算，因此 DECIMAL 的计算比浮点类型需要更高的代价。

FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽，例如 DECIMAL(18, 9) 表示总共 18 位，取 9 位存储小数部分，剩下 9 位存储整数部分。

## 字符串

主要有 CHAR 和 VARCHAR 两种类型，一种是定长的，一种是变长的。

VARCHAR 这种变长类型能够节省空间，因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长，当超出一个页所能容纳的大小时，就要执行额外的操作。MyISAM 会将行拆成不同的片段存储，而 InnoDB 则需要分裂页来使行放进页内。

在进行存储和检索时，会保留 VARCHAR 末尾的空格，而会删除 CHAR 末尾的空格。

## 时间和日期

MySQL 提供了两种相似的日期时间类型：DATETIME 和 TIMESTAMP。

### 1. DATETIME

能够保存从 1000 年到 9999 年的日期和时间，精度为秒，使用 8 字节的存储空间。

它与时区无关。

默认情况下，MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值，例如“2008-01-16 22:37:08”，这是 ANSI 标准定义的日期和时间表示方法。

### 2. TIMESTAMP

和 UNIX 时间戳相同，保存从 1970 年 1 月 1 日午夜（格林威治时间）以来的秒数，使用 4 个字节，只能表示从 1970 年到 2038 年。

它和时区有关，也就是说一个时间戳在不同的时区所代表的具体时间是不同的。

MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期，并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。

默认情况下，如果插入时没有指定 TIMESTAMP 列的值，会将这个值设置为当前时间。

应该尽量使用 TIMESTAMP，因为它比 DATETIME 空间效率更高。

# 五、切分
## 分库分表中间件

简单易用的组件：
当当sharding-jdbc
蘑菇街TSharding

强悍重量级的中间件：
sharding
TDDL Smart Client的方式（淘宝）
Atlas(Qihoo 360)
alibaba.cobar(是阿里巴巴（B2B）部门开发)
MyCAT（基于阿里开源的Cobar产品而研发）
Oceanus(58同城数据库中间件)
OneProxy(支付宝首席架构师楼方鑫开发)
vitess（谷歌开发的数据库中间件）

## 水平切分

水平切分又称为 Sharding，它是将同一个表中的记录拆分到多个结构相同的表中。

当一个表的数据不断增多时，Sharding 是必然的选择，它可以将数据分布到集群的不同节点上，从而缓存单个数据库的压力。

## 垂直切分

垂直切分是将一张表按列切分成多个表，通常是按照列的关系密集程度进行切分，也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。

在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中，例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。

## Sharding 策略

### 顺序分库分表
按照id顺序分库分表,但要保证id是有顺序自增的,而且会导致顺序分布将来的数据库访问会面临某个数据库访问过多过热的情况.

按照时间来分库,一月一存.

如果无法保证你的id是自增长的，那么你的数据就会凌乱的分散在各个数据库，这样热点确实就分散了，可是每当你增加一个数据库的时候，你就有可能进行大量的数据迁移，应对这种情况，就是尽量减少数据迁移的代价，所以这里运用一致性hash的方式进行分库分表比较好，可以尽可能的减少数据迁移，并且也能让解决热点过于集中的问题.一致性hash的分库策略可以看redis的md.

### 取模分库分表
一般的取模分库分表是就是将id mod n(一般为2的次数)，然后放入数据库中，这样能够使数据分散，不会有某个数据库过热的问题

二次分库时，为了数据迁移方便，一般是按倍数增加，比如初始4个库，二次分裂为8个，再16个。这样对于某个库的数据，一半数据移到新库，剩余不动，对比每次只增加一个库，所有数据都要大规模变动。

### 映射表：使用单独的一个数据库来存储映射关系。

性能会有所下降,因为要查两个数据库

## Sharding 存在的问题

### 1. 事务问题

使用分布式事务来解决，比如 XA 接口。

方案一：使用分布式事务
优点：交由数据库管理，简单有效
缺点：性能代价高，特别是shard越来越多时

方案二：由应用程序和数据库共同控制
原理：将一个跨多个数据库的分布式事务分拆成多个仅处 于单个数据库上面的小事务，并通过应用程序来总控 各个小事务。
优点：性能上有优势
缺点：需要应用程序在事务控制上做灵活设计。如果使用 了spring的事务管理，改动起来会面临一定的困难。

### 2. 连接

可以将原来的连接分解成多个单表查询，然后在用户程序中进行连接。

### 3. ID 唯一性

- 使用全局唯一 ID（GUID）
- 为每个分片指定一个 ID 范围
- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)

### 跨库join
只要是进行切分，跨节点Join的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。

### 跨节点的count,order by,group by以及聚合函数问题
这些是一类问题，因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。解决方案：与解决跨节点join问题的类似，分别在各个节点上得到结果后在应用程序端进行合并。和join不同的是每个结点的查询可以并行执行，因此很多时候它的速度要比单一大表快很多。但如果结果集很大，对应用程序内存的消耗是一个问题。

### 数据迁移，容量规划，扩容等问题

来自淘宝综合业务平台团队，它利用对2的倍数取余具有向前兼容的特性（如对4取余得1的数对2取余也是1）来分配数据，避免了行级别的数据迁移，但是依然需要进行表级别的迁移，同时对扩容规模和分表数量都有限制。总得来说，这些方案都不是十分的理想，多多少少都存在一些缺点，这也从一个侧面反映出了Sharding扩容的难度。

# 六、复制

## 主从复制

主要涉及三个线程：binlog 线程、I/O 线程和 SQL 线程。

-  **binlog 线程** ：负责将主服务器上的数据更改写入二进制日志（Binary log）中。
-  **I/O 线程** ：负责从主服务器上读取二进制日志，并写入从服务器的中继日志（Relay log）。
-  **SQL 线程** ：负责读取中继日志，解析出主服务器已经执行的数据更改并在从服务器中重放（Replay）。

主从同步事件有3种形式:statement、row、mixed。

statement：会将对数据库操作的sql语句写入到binlog中。
row：会将每一条数据的变化写入到binlog中。
mixed：statement与row的混合。Mysql决定什么时候写statement格式的，什么时候写row格式的binlog

## 读写分离

主服务器处理写操作以及实时性要求比较高的读操作，而从服务器处理读操作。

读写分离能提高性能的原因在于：

- 主从服务器负责各自的读和写，极大程度缓解了锁的争用；
- 从服务器可以使用 MyISAM，提升查询性能以及节约系统开销；
- 增加冗余，提高可用性。

读写分离常用代理方式来实现，代理服务器接收应用层传来的读写请求，然后决定转发到哪个服务器。

# 事务隔离机制

## 事务必须满足ACID
原子性（Atomicity）：事务作为一个整体被执行，包含在其中的对数据库的操作要么全部被执行，要么都不执行

一致性（Consistency）：事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束

隔离性（Isolation）：多个事务并发执行时，一个事务的执行不应影响其他事务的执行

持久性（Durability）：已被提交的事务对数据库的修改应该永久保存在数据库中

## 事务的定义

事务（Transaction）是并发控制的基本单位。所谓的事务，它是一个操作序列，这些操作要么都执行，要么都不执行，它是一个不可分割的工作单位。例如，银行转账工作：从一个账号扣款并使另一个账号增款，这两个操作要么都执行，要么都不执行。所以，应该把它们看成一个事务。事务是数据库维护数据一致性的单位，在每个事务结束时，都能保 持数据一致性。

事务的生命周期

## 并发事务会出现的问题

### 第一类丢失更新
定义：A事务撤销时，把已经提交的B事务的更新数据覆盖了。

以上的示例演示了第一类丢失更新问题，事务B虽然成功了，但是它所做的更新没有被永久存储，这种并发问题是由于完全没有隔离事务造成的。

当两个事务更新相同的数据时，如果一个事务被提交，另一个事务却撤销，那么会连同第一个事务所做的更新也被撤销了。（这是绝对避免出现的事情） 事务A的开始时间和结束时间包含事务B的开始和结束时间,事务A回滚事务的同时,把B的已经提交的事务也回滚的,这是避免的,这就是第一类丢失更新.

### 第二类丢失更新
定义：A事务提交时，把已经提交的B事务的更新数据覆盖了。

第二类丢失更新和第一类的区别实际上是对数据的影响是由A事务的撤销还是提交造成的，它和不可重复读(下面介绍)本质上是同一类并发问题，通常把它看做是不可重复读的一个特例。两个或多个事务查询同一数据。然后都基于自己的查询结果更新数据，这时会造成最后一个提交的更新事务，将覆盖其它已经提交的更新事务。

### 脏读：
脏读就是指当一个事务正在访问数据，并且对数据进行了修改，而这种修改还没有提交到数据库中，这时，另外一个事务也访问这个数据，然后使用了这个数据。

### 不可重复读：
是指**在一个事务内，多次读同一数据,结果却不是重复的一样的值**。在这个事务还没有结束时，另外一个事务也访问该同一数据。那么，在第一个事务中的两次读数据之间，由于第二个事务的修改，那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的，因此称为是不可重复读。（即不能读到相同的数据内容）

### 幻读:
定义：读到已提交插入数据，幻读与不可重复读类似，幻读是查询到了另一个事务已提交的新插入数据，而不可重复读是查询到了另一个事务已提交的更新数据。

**不可重复读和幻读的区别： 简单来说，不可重复读是由于数据修改引起的，幻读是由数据行插入或者删除引起的。**

## 四种事务隔离机制

读未提交:事物开始/假/真/事务提交

读提交:事务开始/旧/真/事务提交

可重复读:事务开始/旧旧旧/事务提交/真.

未提交读(Read Uncommitted)：允许脏读，也就是可能读取到其他会话中未提交事务修改的数据

提交读(Read Committed)：只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)

可重复读(Repeated Read)：可重复读。在同一个事务内的查询都是事务开始时刻一致的，InnoDB默认级别。在SQL标准中，该隔离级别消除了不可重复读，但是还存在幻象读

串行读(Serializable)：完全串行化的读，每次读都需要获得表级共享锁，读写相互都会阻塞

### 读未提交

该隔离级别，所有事务都可以看到其他未提交事务的执行结果。通俗地讲就是，在一个事务中可以读取到另一个事务中新增或修改但未提交的数据。

该隔离级别可能导致的问题是脏读。因为另一个事务可能回滚，所以在第一个事务中读取到的数据很可能是无效的脏数据，造成脏读现象

```
> set tx_isolation='READ-UNCOMMITTED';
```

### 读已提交

这是大多数数据库系统的默认隔离级别（但不是mysql默认的）

一个事务只能看见已经提交事务所做的修改。换句话说，一个事务从开始直到提交之前，所做的任何修改对其他事务都是不可见的。

该隔离级别可能导致的问题是不可重复读。因为两次执行同样的查询，可能会得到不一样的结果。

notes\pictures\mysql\Snipaste_2019-09-08_10-29-01.jpg

### Repeatable read 可重复读

这是MySQL的默认事务隔离级别

它确保同一事务的多个实例在并发读取数据时，会看到同样的数据行。通俗来讲，可重复读在一个事务里读取数据,怎么读都不会变，除非提交了该事务才会发现其他事务修改了数据,仿佛产生幻觉(我之前查了那么多次都没发现改变啊)  。

该隔离级别存在的问题是幻读

虽然Repeatable read避免了不可重复读，但还有可能出现幻读 。

### Serializable 串行化
这是最高的隔离级别

它通过强制事务排序，使之不可能相互冲突，从而解决幻读问题。通俗地讲就是，假如两个事务都操作到同一数据行，那么这个数据行就会被锁定，只允许先读取/操作到数据行的事务优先操作，只有当事务提交了，数据行才会解锁，后一个事务才能成功操作这个数据行，否则只能一直等待

该隔离级别可能导致大量的超时现象和锁竞争。
## 事务隔离机制的实现原理

Read Uncommitted，读写均不使用锁，数据的一致性最差，也会出现许多逻辑错误。

Read Committed，使用写锁，但是读会出现不一致，不可重复读。

Repeatable Read, 使用读锁和写锁，解决不可重复读的问题，但会有幻读。

Serializable, 使用事务串形化调度，避免出现因为插入数据没法加锁导致的不一致的情况。

太难了,有空看
不同事务隔离级别分别会加哪些锁
可重复读与mvcc和一致性锁有关
https://www.cnblogs.com/cjsblog/p/8365921.html

## MVCC
### 定义
Multi-Version Concurrency Control,在InoDb引擎的可重复读事务机制中起重要作用,与行锁一起实现了Innodb的重复读机制,可以在大多数情况下代替行锁,提高效率.

### 实现
利用保存时间快照,经典的有乐观并发控制和悲观并发控制.

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列，分别保存了这个行的创建时间，一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID)，每开始一个新的事务，系统版本号就会自动递增，事务开始时刻的系统版本号会作为事务的ID.下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的.

#### insert
InnoDB为新插入的每一行保存当前系统版本号作为版本号.
第一个事务ID为1；

```sql
start transaction;
insert into yang values(NULL,'yang') ;
insert into yang values(NULL,'long');
insert into yang values(NULL,'fei');
commit;
```

#### select

InnoDB会根据以下两个条件检查每行记录:

a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号)，这样可以确保事务读取的行，要么是在事务开始前已经存在的，要么是事务自身插入或者修改过的.

b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行，在事务开始之前未被删除.

只有a,b同时满足的记录，才能返回作为查询结果.

#### delete
InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.

**举例:**

第二个事务,ID为2;

```sql
start transaction;
select * from yang;  //(1)
select * from yang;  //(2)
commit;
```
**假设1(验证select)**
假设在执行这个事务ID为2的过程中,刚执行到(1),这时,有另一个事务ID为3往这个表里插入了一条数据;
第三个事务ID为3;

```sql
start transaction;
insert into yang values(NULL,'tian');
commit;
```
这时表中的数据如下:

然后接着执行事务2中的(2),由于id=4的数据的创建时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,所以id=4的数据行并不会在执行事务2中的(2)被检索出来,在事务2中的两条select 语句检索出来的数据都只会下表:

**假设2(验证delete)**
假设在执行这个事务ID为2的过程中,刚执行到(1),假设事务执行完事务3后，接着又执行了事务4;
第四个事务:
```sql
start   transaction;
delete from yang where id=1;
commit;
```
此时数据库中的表如下:

写代码,写日志,本地debug,发到uat,uat测试通过,code review,发到pre,pre测试通过,发到pro,测试通过,删除次要日志,留下关键日志(info里也有很多关键日志),merge到master(merge交给别人来做)

接着执行事务ID为2的事务(2),根据SELECT 检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行,而id=4的行上面已经说过,而id=1的行由于删除时间(删除事务的ID)大于当前事务的ID,所以事务2的(2)select * from yang也会把id=1的数据检索出来.所以,事务2中的两条select 语句检索出来的数据都如下:

#### update
InnoDB执行UPDATE，实际上是新插入了一行记录，并保存其创建时间为当前事务的ID，同时保存当前事务ID到要UPDATE的行的删除时间.

**假设3(验证update)**
假设在执行完事务2的(1)后又执行,其它用户执行了事务3,4,这时，又有一个用户对这张表执行了UPDATE操作:
第5个事务:

```sql
start  transaction;
update yang set name='Long' where id=2;
commit;
```
根据update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID,得到表如下:

继续执行事务2的(2),根据select 语句的检索条件,得到下表:

还是和事务2中(1)select 得到相同的结果.

### mvcc解决幻读
例如：

此时books表中有5条数据，版本号为1

事务A，系统版本号2：select * from books；因为1<=2所以此时会读取5条数据。
事务B，系统版本号3：insert into books ...，插入一条数据，新插入的数据版号为3，而其他的数据的版本号仍然是2，插入完成之后commit，事务结束。

事务A，系统版本号2：再次select * from books；只能读取<=2的数据，事务B新插入的那条数据版本号为3，因此读不出来，解决了幻读的问题。
## 数据库的锁
### 乐观锁与悲观锁

乐观锁, 顾名思义，就是很乐观，每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据，可以使用版本号等机制。乐观锁适用于多读的应用类型，这样可以提高吞吐量，像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

悲观锁，顾名思义，就是很悲观，每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制，比如**行锁，表锁等，读锁，写锁等，都是在做操作之前先上锁。**

### 共享锁(读锁,S锁)
**被加锁的对象可以被持锁事务读取，但是不能被修改**，其他事务也可以在上面再加共享锁。

兼容性：如果数据资源上放置了共享锁，还能再放置共享锁和更新锁。

并发性能：具有良好的并发性能，当数据被放置共享锁后，还可以再放置共享锁或更新锁。所以并发性能很好。

### 排他锁(写锁,X锁)
**被加锁的对象只能被持有锁的事务读取和修改，其他事务无法在该对象上加其他锁，也不能读取和修改该对象**

兼容性：独占锁不能和其他锁兼容，如果数据资源上已经加了独占锁，就不能再放置其他的锁了。同样，如果数据资源上已经放置了其他锁，那么也就不能再放置独占锁了。

并发性能：最差。只允许一个事务访问锁定的数据，如果其他事务也需要访问该数据，就必须等待。

### 更新锁(意向锁)
更新锁在的初始化阶段用来锁定可能要被修改的资源，这可以避免使用共享锁造成的死锁现象。例如，对于以下的update语句：

UPDATE accounts SET balance=900 WHERE id=1

更新操作需要分两步：读取accounts表中id为1的记录 –> 执行更新操作。

如果在第一步使用共享锁，再第二步把锁升级为独占锁，就可能出现死锁现象。例如：两个事务都获取了同一数据资源的共享锁，然后都要把锁升级为独占锁，但需要等待另一个事务解除共享锁才能升级为独占锁，这就造成了死锁。

更新锁有如下特征：

加锁与解锁：当一个事务执行update语句时，数据库系统会先为事务分配一把更新锁。当读取数据完毕，执行更新操作时，会把更新锁升级为独占锁。

兼容性：更新锁与共享锁是兼容的，也就是说，一个资源可以同时放置更新锁和共享锁，但是最多放置一把更新锁。这样，当多个事务更新相同的数据时，只有一个事务能获得更新锁，然后再把更新锁升级为独占锁，其他事务必须等到前一个事务结束后，才能获取得更新锁，这就避免了死锁。

并发性能：允许多个事务同时读锁定的资源，但不允许其他事务修改它。

### 锁粒度
表级锁：开销小，加锁快；不会出现死锁；锁定粒度大，发生锁冲突的概率最高，并发度最低。
这些存储引擎通过总是一次性同时获取所有需要的锁以及总是按相同的顺序获取表锁来避免死锁。

表级锁更适合于以查询为主，并发用户少，只有少量按索引条件更新数据的应用，如Web 应用

行级锁：开销大，加锁慢；会出现死锁；锁定粒度最小，发生锁冲突的概率最低，并发度也最高。
最大程度的支持并发，同时也带来了最大的锁开销。
在 InnoDB 中，除单个 SQL 组成的事务外，
锁是逐步获得的，这就决定了在 InnoDB 中发生死锁是可能的。
行级锁只在存储引擎层实现，而Mysql服务器层没有实现。 行级锁更适合于有大量按索引条件并发更新少量不同数据，同时又有并发查询的应用，如一些在线事务处理（OLTP）系统

页面锁：开销和加锁时间界于表锁和行锁之间；会出现死锁；锁定粒度界于表锁和行锁之间，并发度一般

# 解决超卖

场景：

一个商品有库存，下单时先检查库存，如果>0，把库存-1然后下单，如果<=0，则不能下单，事务包含两条sql语句：

```
select quantity from products WHERE id=3;
update products set quantity = ($quantity-1) WHERE id=3;
```

在并发情况下，可能会把库存减为负数(两个进程同时select出来的都>0,然后都会执行update)，怎么办呢？

## 方法1：

InnoDB支持通过特定的语句进行显示加锁：

select...lock in share mode

select...for udpate

```
select quantity from products WHERE id=3 for update;
update products set quantity = ($quantity-1) WHERE id=3;
```

但是执行for update会产生一些其他的影响

1.select语句变慢

2.一些优化无法正常使用,例如索引覆盖扫描

3.很容易造成服务器的锁争用问题

## 方法二：

把udpate语句写在前边，先把数量-1，之后select出库存如果>-1就commit,否则rollback。

```
update products set quantity = quantity-1 WHERE id=3;
select quantity from products WHERE id=3 for update;
```

上边的事务中先执行了update，所以id=3的行被加了行锁,只有commit/rollback是才会被释放（事务中锁是逐步获得的，但是都是commit时所释放的）。很好的解决了并发问题。

<file_sep>/codes/javacode/jichu/BFSTree.java
package javacode.jichu;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;

/**
 * @author <EMAIL>
 * @date 19/9/16/016 15:06
 */
public class BFSTree {
    class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
    }

    public void bfs(TreeNode root) {
        if (root == null) {
            return ;
        }
        LinkedList<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            TreeNode temp = queue.poll();
            System.out.println(temp.val);
            if (temp.left != null) {
                queue.offer(temp.left);
            }
            if (temp.right != null) {
                queue.offer(temp.right);
            }
        }
    }
}
<file_sep>/notes/上海公司.md

选择公司部门的心得:

```
新兴潜力业务>核心老业务>边缘业务

已经崛起的部门,没太必要去了.内斗,去了也是修修补补.

阿里云,闲鱼,蚂蚁金服,钉钉(潜力比较大),蚂蚁金服金融和技术拆分,最盈利的金融业务没了.
我们组几个蚂蚁过来的,租房贵,外卖贵,生活成本高,招人少,钱给的也少
阿里云,蚂蚁未上市.阿里妈妈,淘系很赚钱

天猫,淘宝(机会不多,空间不大)

阿里旅行,饿了么,uc  保守态度.

拼多多上市了 股票没了
```

```
别选太累的实习,没时间准备毕设和秋招.

2019.01-2019.03        大数据清洗算法的Hadoop与JVM性能调优     复旦-数据库安全实验室
项目描述:数据清洗算法在JVM内存使用,IO,计算等方面有其独特的规律,针对其规律做底层调优以提高算法运行效率
*  负责JVM内存分析与对象生命周期监控,对现有垃圾收集算法作出改进以适应大数据清洗算法
*  负责Hadoop，Hive，Yarn性能调优
```

```
我要不来说几个我看好的：
1.阿里巴巴：蚂蚁金服，阿里妈妈，阿里云
2.腾讯: 游戏，微信，金融
3.网易: 游戏
4.百度：不看好
5.微软：不看好
6.创业公司：依图，七牛，glow, 今日头条，蘑菇街，滴滴打车（以及滴滴机器学习研究院），Face++

7.Google, Facebook, 个人水平有限，难以控制自身去某个部门。

作者：<NAME>
链接：https://www.zhihu.com/question/35154312/answer/62007689
来源：知乎
著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。
```

![image-20200208140251314](/Users/sunxu/Library/Application Support/typora-user-images/image-20200208140251314.png)

最优解么是实习就近好部门，留用。 保守一点么命运还是窝在自己手里比较好

然后还有种我们实验室的情况可能你不太会碰到，因为你放养的嘛，就是一般实习满3个月才能申请转正，那像我们7月份出去，正式申请转正9，10月份了，mentor口头告诉你转正问题不大，那你信吗.   所以还是要尽早去,不然没有hc

这情况我们实验室上一届也碰到过，到了很晚才提正式转正的事儿。 一么尽早去，二么秋招还是要足够上心给自己一个后路，万一实习转正这事儿出了叉子的话

是啊，最早也是6月底了，所以我们实验室他们想好好干实习的也确实很难兼顾秋招提前批,这才刚去实习，才刚上手呢就要准备秋招，还有论文，是很困难.

蚂蚁金服招人少,钱给的也少

![image-20200208175600265](/Users/sunxu/Library/Application Support/typora-user-images/image-20200208175600265.png)

# 上海公司

作者：凝眸
链接：https://www.zhihu.com/question/19596230/answer/92273392
来源：知乎
著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。

以下不包含游戏公司。

- **一、独角兽：**

1. **字节跳动**：今日头条母公司，上海主要是短视频+广告+教育+游戏，坐标漕河泾。
2. **蚂蚁金服**：金融监管和支付网联之后，会更加聚焦在金融科技，国际化。坐标商城路S空间。
3. **饿了么**：O2O／外卖，2018年被阿里收购。计划和口碑整合。坐标近铁城市广场。
4. **小红书**：跨境电商+生活社区，2018年5月获得阿里领投的3亿美元融资。坐标新天地SOHO复兴广场。

- **二、上市公司：**

1. **Alibaba上海**：包括口碑，大文娱等，大虹桥有个总部办公楼。
2. **Tencent上海**：坐标漕河泾腾讯大厦。
3. **美团点评**：到店，到家业务。坐标北新泾。
4. **京东上海**：主要是京东商城的无线部门，坐标淞虹路明基大厦。
5. **爱奇艺**：视频网站，对标Netflix，2018年3月29日已在美股上市。坐标淞虹路中关村虹桥创新中心。
6. **携程**：OTA巨头。坐标淞虹路凌空SOHO。
7. **Bilibili**：视频+直播+游戏。坐标五角场国正中心。

(更新自2020/03/23)

------

原答案：

**写在前面**

1、不打广告不吹逼

2、有两类公司未做推荐，一类是答主不熟悉的游戏公司，譬如巨人、骏梦、第九城市等等，另一类是传统公司转型做互联网，思路还不是很清晰，也不推荐，譬如万达新飞凡。

- **一、行业巨无霸型互联网公司：**

1. 蚂蚁金服：互联网金融领域，口碑也在这个体系里，16、17年财富事业部、海外事业部、保险事业部招聘缺口貌似很大；
2. 陆金所：互联网金融领域／P2P，计划在香港上市；
3. 携程：互联网旅游领域，收购了去哪儿网之后真的是一家独大；
4. 大众点评：团购／O2O／本地生活，装机率很高的App，美食评价很具有参考性。

- **二、上市公司的上海分公司：**

1. Baidu上海：地点在张江，有自己的办公楼，业务应该是LBS相关，包括百度浏览器等；
2. Alibaba上海：若干个项目组，包括收购的天天动听、阿里影业等等等；
3. Tencent上海：地点在漕河泾，业务线主要是游戏、互动娱乐；
4. 京东上海：地址在淞虹路，靠近携程，主要是京东商城的无线部门；
5. 平安集团：包括平安金融科技、平安科技、平安健康、平安普惠等，技术体系的办公职场主要在徐汇滨江（龙华中路地铁站），经评论提醒，平安科技办公点在张江和崂山路。
6. 唯品会：电子商务／互联网金融，上海办公地点临近大悦城，未来会搬迁到大虹桥的新办公楼。

- 三、**互联网+海淘：**

1. 小红书：一家颜值较高的互联网创业公司，里面很很多网红喔；
2. 洋码头：较大的互联网海淘平台；

- **四、互联网+教育：**

1. 沪江：最早的网校起家，现在是中国较大的互联网教育平台，据说技术氛围不错；
2. 学霸君：位于五角场创智天地，技术氛围不错；
3. 英语流利说：互联网教育公司。

- **五、互联网＋工具**

1. PPTV：视频平台，母公司为聚力，已被苏宁收购；
2. 爱奇艺：视频平台，是百度的子公司，未来可能会分拆上市；
3. 天天动听（阿里星球）：音乐播放平台，隶属于阿里音乐事业部，董事长是高晓松；
4. 喜马拉雅FM：音频播放平台；
5. 蜻蜓FM：音频播放平台；
6. 格瓦拉：本地生活订票平台，已和微影时代合并，但保持独立运营；
7. Teambition：团队协作工具，一家专注且踏实做产品的创业公司，CEO不错。
8. bilibili：一家身披二次元标签的视频公司，潜力巨大。
9. Wi-Fi万能钥匙：陈天桥的弟弟陈大年先生创办的，android装机率蛮高的。至于公司能走多远，发展成什么规模，就让我们拭目以待吧！
10. 七牛云存储：张江的一家中型创业公司，老板是盛大出来的go语言专家。

- **六、较大的互联网公司**

1. 饿了么：O2O／外卖，阿里系；
2. 滴滴出行：出行领域，上海有海浪事业部；
3. 安居客：房产领域，已被58收购（2016下半年开始996，17年好像又变成了大小周）；
4. 驴妈妈旅行网：互联网旅游，2015年行业排名第五；
5. 百姓网：分类信息网站，上海著名慢公司；
6. 虎扑体育：体育社区。

- **七、互联网金融**

1. 麦子金服：发展迅速的互联网金融公司，据说风控管理做得很棒。旗下明星产品是财神爷爷、诺诺镑客、大房东、名校贷。
2. 点融网：总部设在上海的互金公司，研发体系据说相对比较轻松。

\------------------------------------------------------------------------

20180126更新:

- **八、新零售**

1、永辉云创：公司坐标位于漕河泾。从15年热门的O2O，到17年大火的新零售，永辉的表现一直很抢眼。私以为做O2O也好，做新零售也好，一定要有丰富的线下经验，而永辉就是这样一家公司，尤其是背靠永辉+腾讯两大上市公司，前景很好。

字节跳动：今日头条母公司，上海主要是短视频和广告业务，坐标八号桥和漕河泾。
蚂蚁金服：金融监管和支付网联之后，会更加聚焦在金融科技，国际化。坐标上海中心大厦。
饿了么：O2O／外卖，2018年被阿里收购。计划和口碑整合。坐标近铁城市广场。
小红书：海淘+社区，2018年5月获得阿里领投的3亿美元融资。坐标新天地SOHO复兴广场。

Alibaba上海：包括收购的天天动听、优酷、阿里影业等等。
Baidu上海：坐标张江。
Tencent上海：坐标漕河泾腾讯大厦。
美团点评：已在香港上市。坐标北新泾（筹）。
京东上海：主要是京东商城的无线部门，坐标淞虹路明基大厦。
爱奇艺：视频网站，对标Netflix，2018年3月29日已在美股上市。坐标漕河泾和中关村虹桥创新中心。
中国平安：旗下的平安好医生，2018年已在香港成功上市。另外几家独角兽公司，也在计划上市，包括陆金所、金融壹账通、平安医保科技等。坐标龙华中路平安大厦。
携程：OTA领域巨头。坐标淞虹路凌空SOHO。
唯品会：电子商务，坐标大虹桥中骏广场。
Bilibili：一家二次元网站，2018年已登录美股纳斯达克。坐标五角场。
趣头条：美股上市，业务主要是下沉渠道内容分发平台。坐标广兰路。

拍拍贷：美股上市的金融科技公司，早期靠P2P起家。坐标张江。

学霸君：位于五角场创智天地，技术氛围不错；

滴滴出行：出行领域，上海有海浪事业部；

# 职业选择

作者：皮皮彭
链接：https://www.zhihu.com/question/52154129/answer/129388960
来源：知乎
著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。

具体情况一般具体分析

追求工作和人生的稳定性，直接选微软即可（大公司，稳定，工作生活平衡），结束

追求事业和个人成就，那么就这么分析

1）公司所做具体事情（技术）的未来前景和钱景，这个包括未来的跳槽

今日头条：基础架构，做私有云。

\>> 前景和钱景都不错，毕竟没上市，而且今日头条还是很愿意砸钱给优秀人生的，表现未来涨薪幅度和可能性比较大；

\>> 技术方向，中上，互联网技术架构比较成熟，差不多也就那些

微软：

\>> 前景未知，做Office？维护Office？维护一个二十多年的产品总不是一件很有前景的事情，涨薪大公司都是比较慢的

\>> 技术方向：呃，Office还有啥叼炸天的功能吗？关键是一年可以写上1000行代码吗？还是修修补补各种奇奇怪怪的bug？

腾讯sng：

\>> 前景：不了解

\>> 技术方向：客户端还是服务器？都比较成熟，和今日头条一样，属于基础素质好谁做都差不多

airbnb

\>> 前景：如果是中国新员工应该不错吧，晋升和钱景估计都还不错（只能说可能比腾讯和微软好，应该不如今日头条）

\>> 技术方向：不好分析，估计都是做业务功能，技术上应该没太大挑战，不好说，也说不定去美国学习学习airbnb大牛的技术平台

2）所做的事情和自己能力的匹配程度

这个由于你的个人信息不清楚，不好评价，基本上就是，比如你擅长机器学习，但是去做了视频直播客户端开发，不合适，你应该找的是大数据机器学习的工作；

你擅长安全，却去搞Office Online，不合适（应该去360或者其他安全部门或者安全公司）

你能力综合平衡，善于沟通和管理而技术又不错，做基础架构这种要钻研的就未必很合适了

这个条件的前提是你有一些深入的专长（通常是研究生以上，有一定研究方向，并且一定程度热爱），同时这个专长又是有一定前景的

选择匹配自己的专长可能使自己的事业事半功倍

3）公司品牌

微软 > 腾讯 > airbnb > 今日头条

这个的重要性在于，未来你一旦一无所成（做的不是你想要的，技术挑战也一般，产品线发展也一般），品牌可能给你带来职业光环（哦，微软出来的，BAT的。。。），跳槽或者创业都带点优势（注意这个也有保质期）

总的来说，如果抛除第二条的考虑因素（信息不足无法给出建议），如果个人拼搏能力强的，想快速赚钱和晋升的，同时兼顾个人技术能力发展的，建议选择今日头条<file_sep>/notes/Java-并发.md

- [进程与线程](#进程与线程)
    - [线程的独立资源](#线程的独立资源)
    - [进程与线程的内存关系](#进程与线程的内存关系)
        - [进程上下文](#进程上下文)
        - [进程上下文切换](#进程上下文切换)
    - [进程与线程的通讯方式](#进程与线程的通讯方式)
        - [进程间的通信方式](#进程间的通信方式)
            - [管道( pipe )：](#管道-pipe-)
            - [有名管道 (namedpipe) ：](#有名管道-namedpipe-)
            - [信号量(semophore ) ：](#信号量semophore--)
            - [消息队列( messagequeue ) ：](#消息队列-messagequeue--)
            - [共享内存(shared memory ) ：](#共享内存shared-memory--)
            - [套接字(socket ) ：](#套接字socket--)
        - [线程间的通信方式](#线程间的通信方式)
- [一、线程状态转换](#一线程状态转换)
    - [新建（New）](#新建new)
    - [运行（Runnable）](#运行runnable)
    - [阻塞（Blocked）](#阻塞blocked)
    - [无限期等待（Waiting）](#无限期等待waiting)
    - [限期等待（Timed Waiting）](#限期等待timed-waiting)
    - [死亡（Terminated）](#死亡terminated)
- [二、使用线程](#二使用线程)
    - [实现 Runnable 接口](#实现-runnable-接口)
    - [实现 Callable 接口](#实现-callable-接口)
    - [继承 Thread 类](#继承-thread-类)
    - [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread)
    - [run与start的区别](#run与start的区别)
    - [new thread的错误](#new-thread的错误)
    - [线程池的优点](#线程池的优点)
- [线程池](#线程池)
    - [自定义线程池](#自定义线程池)
        - [handler的拒绝策略：](#handler的拒绝策略)
    - [线程池submit与run的区别](#线程池submit与run的区别)
    - [预定义线程池](#预定义线程池)
- [三、基础线程机制](#三基础线程机制)
    - [Executor](#executor)
    - [Daemon](#daemon)
    - [sleep()](#sleep)
    - [yield()](#yield)
- [四、中断](#四中断)
    - [Interrupt()](#interrupt)
    - [interrupted()与isInterrupted()](#interrupted与isinterrupted)
        - [区别](#区别)
    - [Executor 的中断操作](#executor-的中断操作)
- [五、互斥同步](#五互斥同步)
    - [synchronized](#synchronized)
    - [ReentrantLock](#reentrantlock)
    - [比较](#比较)
    - [使用选择](#使用选择)
- [六、线程之间的协作](#六线程之间的协作)
    - [join()](#join)
    - [wait() notify() notifyAll()](#wait-notify-notifyall)
    - [await() signal() signalAll()](#await-signal-signalall)
- [七、J.U.C - AQS](#七juc---aqs)
    - [CountDownLatch](#countdownlatch)
    - [CyclicBarrier](#cyclicbarrier)
    - [Semaphore](#semaphore)
- [八、J.U.C - 其它组件](#八juc---其它组件)
    - [FutureTask](#futuretask)
    - [BlockingQueue](#blockingqueue)
    - [ForkJoin](#forkjoin)
- [九、线程不安全示例](#九线程不安全示例)
- [十、Java 内存模型](#十java-内存模型)
    - [主内存与工作内存](#主内存与工作内存)
    - [内存间交互操作](#内存间交互操作)
    - [内存模型三大特性](#内存模型三大特性)
        - [1. 原子性](#1-原子性)
        - [AtomicInteger 能保证多个线程修改的原子性。](#atomicinteger-能保证多个线程修改的原子性)
        - [互斥锁保证操作的原子性](#互斥锁保证操作的原子性)
        - [2. 可见性](#2-可见性)
            - [volatile 与 synchronized的区别](#volatile-与-synchronized的区别)
        - [3. 有序性](#3-有序性)
    - [先行发生原则](#先行发生原则)
        - [1. 单一线程原则](#1-单一线程原则)
        - [2. 管程锁定规则](#2-管程锁定规则)
        - [3. volatile 变量规则](#3-volatile-变量规则)
        - [4. 线程启动规则](#4-线程启动规则)
        - [5. 线程终止规则](#5-线程终止规则)
        - [6. 线程中断规则](#6-线程中断规则)
        - [7. 对象终结规则](#7-对象终结规则)
        - [8. 传递性](#8-传递性)
- [十一、线程安全](#十一线程安全)
    - [不可变](#不可变)
    - [悲观锁(阻塞同步)](#悲观锁阻塞同步)
        - [synchronized的底层原理](#synchronized的底层原理)
        - [synchronized悲观锁的使用场景](#synchronized悲观锁的使用场景)
    - [乐观锁(非阻塞同步)](#乐观锁非阻塞同步)
        - [乐观锁的使用场景](#乐观锁的使用场景)
    - [乐观锁的两种实现方式](#乐观锁的两种实现方式)
        - [CAS(compareAndSwap)](#cascompareandswap)
            - [定义](#定义)
            - [底层实现](#底层实现)
            - [ABA问题](#aba问题)
            - [循环时间开销大](#循环时间开销大)
            - [只能保证一个共享变量的原子操作](#只能保证一个共享变量的原子操作)
        - [版本号机制(解决ABA问题)](#版本号机制解决aba问题)
    - [无同步方案](#无同步方案)
        - [1. 栈封闭](#1-栈封闭)
        - [2. 线程本地存储（Thread Local Storage）](#2-线程本地存储thread-local-storage)

# 进程与线程

进程是资源（CPU、内存等）分配的基本单位，它是程序执行时的一个实例。

线程是程序执行时的最小单位，它是进程的一个执行流，是CPU调度和分派的基本单位，一个进程可以由很多个线程组成，

线程间共享进程的所有资源，**每个线程有自己的堆栈和局部变量**。

进程有自己的独立地址空间，每启动一个进程，系统就会为它分配地址空间，**建立数据表来维护代码段、堆栈段和数据段**，这种操作非常昂贵。而线程是共享进程中的数据的，使用相同的地址空间，因此CPU切换一个线程的花费远比进程要小很多，同时创建一个线程的开销也比进程要小很多。

线程之间的通信更方便，**同一进程下的线程共享全局变量、静态变量等数据，**而进程之间的通信需要以通信的方式（IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

## 线程的独立资源

线程自己基本上不拥有系统资源，但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源

1.线程ID

每个线程都有自己的线程ID，这个ID在本进程中是唯一的。进程用此来标识线程。

2.寄存器组的值(程序计数器)

由于线程间是并发运行的，每个线程有自己不同的运行线索，当从一个线程切换到另一个线程上 时，必须将原有的线程的寄存

器集合的状态保存，以便将来该线程在被重新切换到时能得以恢复。

3.线程的堆栈

堆栈是保证线程独立运行所必须的。线程函数可以调用函数，而被调用函数中又是可以层层嵌套的，所以线程必须拥有自己的

函数堆栈， 使得函数调用可以正常执行，不受其他线程的影响。

4.错误返回码

由于同一个进程中有很多个线程在同时运行，可能某个线程进行系统调用后设置了errno值，而在该 线程还没有处理这个错误，

另外一个线程就在此时被调度器投入运行，这样错误值就有可能被修改。所以，不同的线程应该拥有自己的错误返回码变量。

5.线程的信号屏蔽码

由于每个线程所感兴趣的信号不同，所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都 共享同样的信号处理器。

6.线程的优先级

由于线程需要像进程那样能够被调度，那么就必须要有可供调度使用的参数，这个参数就是线程的优先级。

## 进程与线程的内存关系

进程是操作系统对一个正在运行的程序的抽象，它是资源分配的最小单位，如寄存器，内存，文件等，每一个进程以进程控制块(PCB)的形式来表现。系统因 PCB 而感知进程的存在，并通过 PCB 来对进程进行调度、控制、分配资源。 若进程执行结束，则通过释放 PCB 来释放进程所占有的各种资源。

操作系统会保持跟踪进程运行所需的所有状态信息，也就是进程的上下文。不同进程并发执行,当操作系统决定把 CPU 的控制权从当前进程转移到某个新进程时，就会进行上下文切换，即保存当前进程的上下文，恢复新进程的上下文。

**线程是进程的执行单元，每个线程都运行在进程的上下文中，共享该进程虚拟地址空间里的代码、数据、堆、共享库和打开的文件**。

**每个线程都有自己的线程上下文，包括一个唯一的整数线程 ID、栈内存、PC(程序计数器)、通用目的寄存器和条件码。**

各自独立的线程栈的内存模型并不整齐清楚，它们都保存在进程虚拟地址空间的栈区域中，通常被相应的线程独立访问，但并不对其他线程设防。

### 进程上下文

进程是在操作系统支持下执行的，进程执行时需要操作系统为其设置相应的执行环境，如系统堆栈、地址映像寄存器、程序计数器、程序状态字、打开文件表以及相关通用寄存器等。 所以，把进程的物理实体与支持进程执行的物理环境合称为进程上下文。

- 上文： 把已执行的进程指令和数据在相关寄存器与堆栈中的内容称为上文。
- 正文： 把正在执行的进程指令和数据在相关寄存器与堆栈中的内容称为正文。
- 下文： 把待执行的进程指令和数据在相关寄存器与堆栈中的内容称为下文。

### 进程上下文切换
进程上下文切换发生在不同的进程之间而不是同一个进程内。

进城上下文切换分成三个步骤：
1） 把被切换进程的相关信息保存到有关存储区，例如该进程的PCB中。
2） 操作系统中的调度和资源分配程序执行，选取新的进程。
3） 将被选中进程的原来保存的正文部分从有关存储区中取出，并送至寄存器与堆栈中,激活被选中进程执行。

进程上下文切换的切换过程涉及由谁来保护和获取进程的正文的问题，也就是如何使寄存器和堆栈等中的数据流入流出 PCB 的存储区。 另外，进程上下文切换还涉及系统调度和分配程序，这些都比较耗费CPU时间。

## 进程与线程的通讯方式
### 进程间的通信方式
[每种方式详细介绍](https://blog.csdn.net/violet_echo_0908/article/details/51201278)

#### 管道( pipe )：

管道是一种半双工的通信方式，数据只能单向流动，而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

1，面向字节流，

2，生命周期随内核

3，自带同步互斥机制。

4，半双工，单向通信，两个管道实现双向通信。

#### 有名管道 (namedpipe) ：

有名管道也是半双工的通信方式，但是它允许无亲缘关系进程间的通信。半双工，单向通信，两个管道实现双向通信。

#### 信号量(semophore ) ：

信号量是一个计数器，可以用来控制多个进程对共享资源的访问。它常作为一种锁机制，防止某进程正在访问共享资源时，其他进程也访问该资源。因此，主要作为进程间以及同一进程内不同线程之间的同步手段。

 1，概念

在内核中创建一个信号量集合（本质是个数组），数组的元素（信号量）都是1，使用P操作进行-1，使用V操作+1，

（1） P(sv)：如果sv的值⼤大于零，就给它减1；如果它的值为零，就挂起该进程的执⾏ 。

（2） V(sv)：如果有其他进程因等待sv而被挂起，就让它恢复运⾏，如果没有进程因等待sv⽽挂起，就给它加1。

PV操作用于同一进程，实现互斥。

PV操作用于不同进程，实现同步。

 2，功能：

对临界资源进行保护

#### 消息队列( messagequeue ) ：

消息队列是由消息的链表，存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

1， 消息队列可以认为是一个全局的一个链表，链表节点钟存放着数据报的类型和内容，有消息队列的标识符进行标记。

2，消息队列允许一个或多个进程写入或者读取消息。

3，消息队列的生命周期随内核。

4，消息队列可实现双向通信。

#### 共享内存(shared memory ) ：

共享内存就是映射一段能被其他进程所访问的内存，这段共享内存由一个进程创建，但多个进程都可以访问。共享内存是最快的 IPC 方式，它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制，如信号两，配合使用，来实现进程间的同步和通信。

#### 套接字(socket ) ：

套解口也是一种进程间通信机制，与其他通信机制不同的是，它可用于不同机器间的进程通信。

### 线程间的通信方式

线程间的通信目的主要是用于线程同步，所以线程没有像进程通信中的用于数据交换的通信机制,线程间通讯可以是同一进程下共有的共享内存,全局变量和静态变量

锁机制：包括互斥锁、条件变量、读写锁

互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据，而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程，直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

信号量机制(Semaphore)：包括无名线程信号量和命名线程信号量

信号机制(Signal)：类似进程间的信号处理

线程间的通信目的主要是用于线程同步，所以线程没有像进程通信中的用于数据交换的通信机制。

# 一、线程状态转换(创建、就绪、运行、阻塞和死亡)

**Java当中，线程通常都有五种状态，创建、就绪、运行、阻塞和死亡。**

## 新建（New）

创建后尚未启动。

## 运行（Runnable）

可能正在运行，也可能正在等待 CPU 时间片。

包含了操作系统线程状态中的 Running 和 Ready。

## 阻塞（Blocked）

等待获取一个排它锁，如果其线程释放了锁就会结束此状态。

## 无限期等待（Waiting）

等待其它线程显式地唤醒，否则不会被分配 CPU 时间片。

| 进入方法 | 退出方法 |
| --- | --- |
| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
| LockSupport.park() 方法 | LockSupport.unpark(Thread) |

## 限期等待（Timed Waiting）

无需等待其它线程显式地唤醒，在一定时间之后会被系统自动唤醒。

调用 Thread.sleep() 方法使线程进入限期等待状态时，常常用“使一个线程睡眠”进行描述。

调用 Object.wait() 方法使线程进入限期等待或者无限期等待时，常常用“挂起一个线程”进行描述。

睡眠和挂起是用来描述行为，而阻塞和等待用来描述状态。

阻塞和等待的区别在于，阻塞是被动的，它是在等待获取一个排它锁。而等待是主动的，通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

| 进入方法 | 退出方法 |
| --- | --- |
| Thread.sleep() 方法 | 时间结束 |
| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll()  |
| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
| LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
| LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |

## 死亡（Terminated）

可以是线程结束任务之后自己结束，或者产生了异常而结束。

# 二、使用线程

有三种使用线程的方法：

- 实现 Runnable 接口；
- 实现 Callable 接口；
- 继承 Thread 类。

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务，不是真正意义上的线程，因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

## 实现 Runnable 接口

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。

```java
public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}
```

```java
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}
```

## 实现 Callable 接口

与 Runnable 相比，Callable 可以有返回值，返回值通过 FutureTask 进行封装。

```java
public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
```

```java
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}
```

## 继承 Thread 类

同样也是需要实现 run() 方法，因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时，虚拟机会将该线程放入就绪队列中等待被调度，当一个线程被调度时会执行该线程的 run() 方法。

```java
public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
```

```java
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}
```

## 实现接口 VS 继承 Thread

实现接口会更好一些，因为：

- Java 不支持多重继承，因此继承了 Thread 类就无法继承其它类，但是可以实现多个接口；
- 类可能只要求可执行就行，继承整个 Thread 类开销过大。

## run与start的区别

1.start（）方法来启动线程，真正实现了多线程运行，这时无需等待run方法体代码执行完毕而直接继续执行下面的代码：

通过调用Thread类的start()方法来启动一个线程，
**这时此线程是处于就绪状态，**
并没有运行。
然后通过此Thread类调用方法run()来完成其运行操作的，
这里方法run()称为线程体，
它包含了要执行的这个线程的内容，
Run方法运行结束，
此线程终止，
而CPU再运行其它线程，

2.run（）方法当作普通方法的方式调用，程序还是要顺序执行，还是要等待run方法体执行完毕后才可继续执行下面的代码：

而如果直接用Run方法，
这只是调用一个方法而已，
程序中依然只有主线程--这一个线程，
其程序执行路径还是只有一条，
这样就没有达到写线程的目的

## new thread的错误
new Thread的弊端？

a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理，可能无限制新建线程，相互之间竞争，及可能占用过多系统资源导致死机或oom。
c. 缺乏更多功能，如定时执行、定期执行、线程中断。
## 线程池的优点
相比new Thread，Java提供的四种线程池的好处在于：
a. 重用存在的线程，减少对象创建、消亡的开销，性能佳。
b. 可有效控制最大并发线程数，提高系统资源的使用率，同时避免过多资源竞争，避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。

## Java线程和操作系统线程的对应关系

### 用户线程和操作系统线程的对应关系

使用用户线程加轻量级进程混合实现
在这种混合实现下，即存在用户线程，也存在轻量级进程。用户线程还是完全建立在用户空间中，因此用户线程的创建、切换、析构等操作依然廉价，并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁，这样可以使用内核提供的线程调度功能及处理器映射，并且用户线程的系统调用要通过轻量级进程来完成，大大降低了整个进程被完全阻塞的风险。在这种混合模式中，用户线程与轻量级进程的数量比是不定的，即为N:M的关系：

**Golang的协程就是使用了这种模型:**

**在用户态，协程能快速的切换，避免了线程调度的CPU开销问题，协程相当于线程的线程。**

### java线程

JDK1.2之前，**程序员们为JVM开发了自己的一个线程调度内核，而到操作系统层面就是用户空间内的线程实现。**

JDK1.2及以后，JVM选择了更加稳健且方便使用的操作系统原生的线程模型，通过系统调用，将程序的线程交给了操作系统内核进行调度. 对于Sun JDK来说，它的Windows版与Linux版都是使用一对一的线程模型实现的，**一条Java线程就映射到一条轻量级进程之中**.

**Java中线程的本质，其实就是操作系统中的轻量级线程**，Linux下是基于`pthread`库实现的**轻量级进程**，Windows下是原生的系统`Win32 API`提供系统调用从而实现**多线程**。

# 线程池

线程池：Java中开辟出了一种管理线程的概念，这个概念叫做线程池，从概念以及应用场景中，我们可以看出，线程池的好处，就是可以方便的管理线程，也可以减少内存的消耗。

## 自定义线程池

那么，我们应该如何创建一个线程池Java中已经提供了创建线程池的一个类：Executor

而我们创建时，一般使用它的子类：ThreadPoolExecutor.

线程池中的corePoolSize就是线程池中的核心线程数量，这几个核心线程，只是在没有用的时候，也不会被回收，

maximumPoolSize就是线程池中可以容纳的最大线程的数量，

keepAliveTime，就是线程池中除了核心线程之外的其他的最长可以保留的时间，因为在线程池中，除了核心线程即使在无任务的情况下也不能被清除，其余的都是有存活时间的，意思就是非核心线程可以保留的最长的空闲时间，而util，就是计算这个时间的一个单位，

workQueue，就是等待队列，任务可以储存在任务队列中等待被执行，执行的是FIFIO原则（先进先出）。

threadFactory，就是创建线程的线程工厂，

最后一个handler,是一种拒绝策略，我们可以在任务满了之后，拒绝执行某些任务。

任务进来时，首先执行判断，判断核心线程是否处于空闲状态，如果不是，核心线程就先就执行任务，如果核心线程已满，则判断任务队列是否有地方存放该任务，若果有，就将任务保存在任务队列中，等待执行，如果满了，在判断最大可容纳的线程数，如果没有超出这个数量，就开创非核心线程执行任务，如果超出了，就调用handler实现拒绝策略。

### handler的拒绝策略：
第一种AbortPolicy:不执行新任务，直接抛出异常，提示线程池已满

第二种DisCardPolicy:不执行新任务，也不抛出异常

第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行

第四种CallerRunsPolicy:直接调用execute来执行当前任务

## 线程池submit与run的区别

## 预定义线程池
CachedThreadPool:可缓存的线程池，该线程池中没有核心线程，非核心线程的数量为Integer.max_value，就是无限大，当有需要时创建线程来执行任务，没有需要时回收线程，适用于耗时少，任务量大的情况。

SecudleThreadPool:周期性执行任务的线程池，按照某种特定的计划执行线程中的任务，有核心线程，但也有非核心线程，非核心线程的大小也为无限大。适用于执行周期性的任务。

SingleThreadPool:只有一条线程来执行任务，适用于有顺序的任务的应用场景。

FixedThreadPool:定长的线程池，有核心线程，核心线程的即为最大的线程数量，没有非核心线程

# 三、基础线程机制

## Executor

Executor 管理多个异步任务的执行，而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰，不需要进行同步操作。

主要有三种 Executor：

- CachedThreadPool：一个任务创建一个线程；
- FixedThreadPool：所有任务只能使用固定大小的线程；
- SingleThreadExecutor：相当于大小为 1 的 FixedThreadPool。

```java
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}
```

## Daemon

守护线程是程序运行时在后台提供服务的线程，不属于程序中不可或缺的部分。

当所有非守护线程结束时，程序也就终止，同时会杀死所有守护线程。

main() 属于非守护线程。

在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。

```java
public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}
```

## sleep()

Thread.sleep(millisec) 方法会休眠当前正在执行的线程，millisec 单位为毫秒。

sleep() 可能会抛出 InterruptedException，因为异常不能跨线程传播回 main() 中，因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

```java
public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
```

## yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分，可以切换给其它线程来执行。该方法只是对线程调度器的一个建议，而且也只是建议具有相同优先级的其它线程可以运行。

```java
public void run() {
    Thread.yield();
}
```

# 四、中断

一个线程执行完毕之后会自动结束，如果在运行过程中发生异常也会提前结束。

## Interrupt()

通过调用一个线程的 interrupt() 来中断该线程，如果该线程处于阻塞、限期等待或者无限期等待状态，那么就会抛出 InterruptedException，从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。如果处于运行状态没有处于阻塞或者等待状态,则只会让interrupted()函数返回true,不会提前结束抛出异常.

对于以下代码，在 main() 中启动一个线程之后再中断它，由于线程中调用了 Thread.sleep() 方法，因此会抛出一个 InterruptedException，从而提前结束线程，不执行之后的语句。

```java
public class InterruptExample {

    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
```

```java
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
    System.out.println("Main run");
}
```

```html
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at InterruptExample.lambda$main$0(InterruptExample.java:5)
    at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)
```

## interrupted()与isInterrupted()

如果一个线程的 run() 方法执行一个无限循环，并且没有执行 sleep() 等会抛出 InterruptedException 的操作，那么调用线程的 interrupt() 方法就无法使线程提前结束。(如上一节代码所示,先运行完"main run"才执行异常)

但是调用 interrupt() 方法会设置线程的中断标记，此时调用 interrupted()与 isInterrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 与isInterrupted()方法来判断线程是否处于中断状态，从而提前结束线程。

```java
public class InterruptExample {

    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }
}
```

```java
public static void main(String[] args) throws InterruptedException {
    Thread thread2 = new MyThread2();
    thread2.start();
    thread2.interrupt();
}
```

```html
Thread end
没有异常抛出,是因为thread2本身没有睡眠或者阻塞,所以interrupt()只能把interrupted()的返回值置位true,却不能让线程停止并抛出异常
```
### 区别

```java
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return isInterrupted(false);
}

private native boolean isInterrupted(boolean ClearInterrupted);
```

interrupted()是静态方法而isInterrupted()是实例方法，他们的实现都是调用同一个native方法。主要的区别是他们的形参ClearInterrupted传的不一样。

interrupted()在返回中断标志位后会清除标志位，isInterrupted()则不清除中断标志位。

## Executor 的中断操作

调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭，但是如果调用的是 shutdownNow() 方法，则相当于调用每个线程的 interrupt() 方法。

以下使用 Lambda 创建线程，相当于创建了一个匿名内部线程。

```java
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("Thread run");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    executorService.shutdownNow();
    System.out.println("Main run");
}
```

```html
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
    at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
```

如果只想中断 Executor 中的一个线程，可以通过使用 submit() 方法来提交一个线程，它会返回一个 Future<?> 对象，通过调用该对象的 cancel(true) 方法就可以中断线程。

```java
Future<?> future = executorService.submit(() -> {
    // ..
});
future.cancel(true);
```

# 五、互斥同步

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问，第一个是 JVM 实现的 synchronized，而另一个是 JDK 实现的 ReentrantLock。

## synchronized

### **1. 同步一个代码块**

```java
public void func() {
    synchronized (this) {
        // ...
    }
}
```

**它只作用于同一个对象，如果调用两个对象上的同步代码块，就不会进行同步。**

对于以下代码，使用 ExecutorService 执行了两个线程，由于调用的是同一个对象的同步代码块，因此这两个线程会进行同步，当一个线程进入同步语句块时，另一个线程就必须等待。

```java
public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
```

```java
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}
```

```html
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```

对于以下代码，两个线程调用了不同对象的同步代码块，因此这两个线程就不需要同步。从输出结果可以看出，两个线程交叉执行。

```java
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}
```

```html
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
```

### **2. 同步一个方法**

```java
public synchronized void func () {
    // ...
}
```

它和同步代码块一样，作用于同一个对象。

### **3. 同步一个类**

```java
public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}
```

**作用于整个类，也就是说两个线程调用同一个类的不同对象上的这种同步语句，也会进行同步**。

```java
public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
```

```java
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}
```

```html
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```

### **4. 同步一个静态方法**

```java
public synchronized static void fun() {
    // ...
}
```

**作用于整个类。**

## ReentrantLock

### 该锁有公平和不公平两种.

#### FairSync 公平锁

公平锁就是每个线程在获取锁时会先查看此锁维护的等待队列，如果为空，或者当前线程线程是等待队列的第一个，就占有锁，否则就会加入到等待队列中，以后会按照FIFO的规则从队列中获取，下面是FairSync 的源码：

#### 非公平锁

https://blog.csdn.net/rickiyeat/article/details/78307739

### ReentrantLock简介

ReentrantLock 是 java.util.concurrent（J.U.C）包中的锁。

```java
public class LockExample {

    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 确保释放锁，从而避免发生死锁。
        }
    }
}
```

```java
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
```

```html
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```

## 比较

**1. 锁的实现**

synchronized 是 JVM 实现的，而 ReentrantLock 是 JDK 实现的。

**2. 性能**

新版本 Java 对 synchronized 进行了很多优化，例如自旋锁等，synchronized 与 ReentrantLock 大致相同。

**3. 等待可中断**

当持有锁的线程长期不释放锁的时候，正在等待的线程可以选择放弃等待，改为处理其他事情。

ReentrantLock 可中断，而 synchronized 不行。

**4. 公平锁**

公平锁是指多个线程在等待同一个锁时，必须按照申请锁的时间顺序来依次获得锁。

synchronized 中的锁是非公平的，ReentrantLock 默认情况下也是非公平的，但是也可以是公平的。

**5. 锁绑定多个条件**

一个 ReentrantLock 可以同时绑定多个 Condition 对象。

## 使用选择

除非需要使用 ReentrantLock 的高级功能，否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制，JVM 原生地支持它，而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题，因为 JVM 会确保锁的释放。

# 六、线程之间的协作

当多个线程可以一起工作去解决某个问题时，如果某些部分必须在其它部分之前完成，那么就需要对线程进行协调。

## join()

在线程中调用另一个线程的 join() 方法，会将当前线程挂起，而不是忙等待，直到目标线程结束。

对于以下代码，虽然 b 线程先启动，但是因为在 b 线程中调用了 a 线程的 join() 方法，b 线程会等待 a 线程结束才继续执行，因此最后能够保证 a 线程的输出先于 b 线程的输出。

```java
public class JoinExample {

    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }

    private class B extends Thread {

        private A a;

        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}
```

```java
public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
}
```

```
A
B
```

## wait() notify() notifyAll()

调用 wait() 使得线程等待某个条件满足，线程在等待时会被挂起，当其他线程的运行使得这个条件满足时，其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

它们都属于 Object 的一部分，而不属于 Thread。

只能用在同步方法或者同步控制块中使用，否则会在运行时抛出 IllegalMonitorStateException。

使用 wait() 挂起期间，线程会释放锁。这是因为，如果没有释放锁，那么其它线程就无法进入对象的同步方法或者同步控制块中，那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程，造成死锁。

```java
public class WaitNotifyExample {

    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }

    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
```

```java
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
```

```html
before
after
```

**wait() 和 sleep() 的区别**

- wait() 是 Object 的方法，而 sleep() 是 Thread 的静态方法；
- wait() 会释放锁，sleep() 不会。

## await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类(通过ReentrantLock来获取)来实现线程之间的协调，可以在 Condition 上调用 await() 方法使线程等待，其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

相比于 wait() 这种等待方式，await() 可以指定等待的条件，因此更加灵活。

使用 Lock 来获取一个 Condition 对象。

```java
public class AwaitSignalExample {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
```

```java
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
```

```html
before
after
```

# 七、J.U.C

## AQS(使用了cas,是大多数juc工具的基础)

java.util.concurrent（J.U.C）大大提高了并发性能，AQS 被认为是 J.U.C 的核心。AbstractQueuedSynchronizer，即抽象队列同步器,是一个用来构建锁和同步器的框架.它底层用了 CAS 技术来保证操作的原子性，同时利用 FIFO 队列实现线程间的锁竞争，将基础的同步相关抽象细节放在 AQS，这也是 ReentrantLock,CountDownLatch,CyclicBarrier,Semaphore 等同步工具实现同步的底层实现机制。

## J.U.C常用组件

### ReentrantLock

ReentrantLock 是 java.util.concurrent（J.U.C）包中的锁。

```java
public class LockExample {

    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 确保释放锁，从而避免发生死锁。
        }
    }
}
```

```java
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
```

```html
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```

##

### CountDownLatch

用来控制一个或者多个线程等待多个线程。

维护了一个计数器 cnt，每次调用 countDown() 方法会让计数器的值减 1，减到 0 的时候，那些因为调用 await() 方法而在等待的线程就会被唤醒。

```java
public class CountdownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("run..");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("end");
        executorService.shutdown();
    }
}
```

```html
run..run..run..run..run..run..run..run..run..run..end
```

### CyclicBarrier

用来控制多个线程互相等待，只有当多个线程都到达时，这些线程才会继续执行。

和 CountdownLatch 相似，都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1，并进行等待，直到计数器为 0，所有调用 await() 方法而在等待的线程才能继续执行。

CyclicBarrier 和 CountdownLatch 的一个区别是，CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用，所以它才叫做循环屏障。

CyclicBarrier 有两个构造函数，其中 parties 指示计数器的初始值，barrierAction 在所有线程都到达屏障的时候会执行一次。

```java
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

public CyclicBarrier(int parties) {
    this(parties, null);
}
```

```java
public class CyclicBarrierExample {

    public static void main(String[] args) {
        final int totalThread = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("before..");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.print("after..");
            });
        }
        executorService.shutdown();
    }
}
```

```html
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
```

### Semaphore

Semaphore 类似于操作系统中的信号量，可以控制对互斥资源的访问线程数。

以下代码模拟了对某个服务的并发请求，每次只能有 3 个客户端同时访问，请求总数为 10。

```java
public class SemaphoreExample {

    public static void main(String[] args) {
        final int clientCount = 3;
        final int totalRequestCount = 10;
        Semaphore semaphore = new Semaphore(clientCount);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalRequestCount; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    System.out.print(semaphore.availablePermits() + " ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}
```

```html
2 1 2 2 2 2 2 1 2 2
```

### FutureTask

在介绍 Callable 时我们知道它可以有返回值，返回值通过 Future<V> 进行封装。FutureTask 实现了 RunnableFuture 接口，该接口继承自 Runnable 和 Future<V> 接口，这使得 FutureTask 既可以当做一个任务执行，也可以有返回值。

```java
public class FutureTask<V> implements RunnableFuture<V>
```

```java
public interface RunnableFuture<V> extends Runnable, Future<V>
```

FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间，那么就可以用 FutureTask 来封装这个任务，主线程在完成自己的任务之后再去获取结果。

```java
public class FutureTaskExample {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 0; i < 100; i++) {
                    Thread.sleep(10);
                    result += i;
                }
                return result;
            }
        });

        Thread computeThread = new Thread(futureTask);
        computeThread.start();

        Thread otherThread = new Thread(() -> {
            System.out.println("other task is running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        otherThread.start();
        System.out.println(futureTask.get());
    }
}
```

```html
other task is running...
4950
```

### BlockingQueue

java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现：

-  **FIFO 队列** ：LinkedBlockingQueue、ArrayBlockingQueue（固定长度）
-  **优先级队列** ：PriorityBlockingQueue

提供了阻塞的 take() 和 put() 方法：如果队列为空 take() 将阻塞，直到队列中有内容；如果队列为满 put() 将阻塞，直到队列有空闲位置。

**使用 BlockingQueue 实现生产者消费者问题**

```java
public class ProducerConsumer {

    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

    private static class Producer extends Thread {
        @Override
        public void run() {
            try {
                queue.put("product");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("produce..");
        }
    }

    private static class Consumer extends Thread {

        @Override
        public void run() {
            try {
                String product = queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("consume..");
        }
    }
}
```

```java
public static void main(String[] args) {
    for (int i = 0; i < 2; i++) {
        Producer producer = new Producer();
        producer.start();
    }
    for (int i = 0; i < 5; i++) {
        Consumer consumer = new Consumer();
        consumer.start();
    }
    for (int i = 0; i < 3; i++) {
        Producer producer = new Producer();
        producer.start();
    }
}
```

```html
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
```

### ForkJoin

**主要用于并行计算中，和 MapReduce 原理类似，都是把大的计算任务拆分成多个小任务并行计算。**

```java
public class ForkJoinExample extends RecursiveTask<Integer> {

    private final int threshold = 5;
    private int first;
    private int last;

    public ForkJoinExample(int first, int last) {
        this.first = first;
        this.last = last;
    }

    @Override
    protected Integer compute() {
        int result = 0;
        if (last - first <= threshold) {
            // 任务足够小则直接计算
            for (int i = first; i <= last; i++) {
                result += i;
            }
        } else {
            // 拆分成小任务
            int middle = first + (last - first) / 2;
            ForkJoinExample leftTask = new ForkJoinExample(first, middle);
            ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
            leftTask.fork();
            rightTask.fork();
            result = leftTask.join() + rightTask.join();
        }
        return result;
    }
}
```

```java
public static void main(String[] args) throws ExecutionException, InterruptedException {
    ForkJoinExample example = new ForkJoinExample(1, 10000);
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    Future result = forkJoinPool.submit(example);
    System.out.println(result.get());
}
```

ForkJoin 使用 ForkJoinPool 来启动，它是一个特殊的线程池，线程数量取决于 CPU 核数。

```java
public class ForkJoinPool extends AbstractExecutorService
```

ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列，用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务，避免和队列所属线程发生竞争。例如下图中，Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务，Thread1 会拿出 Task2 来执行，这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。

# 九、线程不安全示例

如果多个线程对同一个共享数据进行访问而不采取同步操作的话，那么操作的结果是不一致的。

以下代码演示了 1000 个线程同时对 cnt 执行自增操作，操作结束之后它的值有可能小于 1000。

```java
public class ThreadUnsafeExample {

    private int cnt = 0;

    public void add() {
        cnt++;
    }

    public int get() {
        return cnt;
    }
}
```

```java
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
```

```html
997
```

# 十、Java 内存模型

Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异，以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

## 主内存与工作内存

处理器上的寄存器的读写的速度比内存快几个数量级，为了解决这种速度矛盾，在它们之间加入了高速缓存。

加入高速缓存带来了一个新的问题：缓存一致性。如果多个缓存共享同一块主内存区域，那么多个缓存的数据可能会不一致，需要一些协议来解决这个问题。

所有的变量都存储在主内存中，每个线程还有自己的工作内存，**工作内存存储在高速缓存或者寄存器中**，保存了该线程使用的变量的主内存副本拷贝。

线程只能直接操作工作内存中的变量，不同线程之间的变量值传递需要通过主内存来完成。

## 内存间交互操作

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。

- read：把一个变量的值从主内存传输到工作内存中
- load：在 read 之后执行，把 read 得到的值放入工作内存的变量副本中
- use：把工作内存中一个变量的值传递给执行引擎
- assign：把一个从执行引擎接收到的值赋给工作内存的变量
- store：把工作内存的一个变量的值传送到主内存中
- write：在 store 之后执行，把 store 得到的值放入主内存的变量中
- lock：作用于主内存的变量
- unlock

## 内存模型三大特性

### 1. 原子性

Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性，例如对一个 int 类型的变量执行 assign 赋值操作，这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据（long，double）的读写操作划分为两次 32 位的操作来进行，即 load、store、read 和 write 操作可以不具备原子性。

有一个错误认识就是，int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中，cnt 属于 int 类型变量，1000 个线程对它进行自增操作之后，得到的值为 997 而不是 1000。

为了方便讨论，将内存间的交互操作简化为 3 个：load、assign、store。

下图演示了两个线程同时对 cnt 进行操作，load、assign、store 这一系列操作整体上看不具备原子性，那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存，T2 依然可以读入旧值。可以看出，这两个线程虽然执行了两次自增运算，但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。

### AtomicInteger 能保证多个线程修改的原子性。

使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现：

```java
public class AtomicExample {
    private AtomicInteger cnt = new AtomicInteger();

    public void add() {
        cnt.incrementAndGet();
    }

    public int get() {
        return cnt.get();
    }
}
```

```java
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicExample example = new AtomicExample(); // 只修改这条语句
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
```

```html
1000
```
### 互斥锁保证操作的原子性

除了使用原子类之外，也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为：lock 和 unlock，在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。

```java
public class AtomicSynchronizedExample {
    private int cnt = 0;

    public synchronized void add() {
        cnt++;
    }

    public synchronized int get() {
        return cnt;
    }
}
```

```java
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicSynchronizedExample example = new AtomicSynchronizedExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
```

```html
1000
```

### 2. 可见性

可见性指当一个线程修改了共享变量的值，其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存，在变量读取前从主内存刷新变量值来实现可见性的。

主要有三种实现可见性的方式：

- volatile
- synchronized，对一个变量执行 unlock 操作之前，必须把变量值同步回主内存。
- final，被 final 关键字修饰的字段在构造器中一旦初始化完成，并且没有发生 this 逃逸（构造器中启动其它线程,通过 this 引用访问到初始化了一半的对象），那么其它线程就能看见 final 字段的值。**关于this逃逸与java编译原理请查找java-虚拟机.md**

对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰，不能解决线程不安全问题，因为 volatile 并不能保证操作的原子性。

#### volatile 与 synchronized的区别
线程安全的两个方面：执行控制和内存可见。

执行控制的目的是控制代码执行（顺序）及是否可以并发执行。

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现，线程在具体执行时，会先拷贝主存数据到线程本地（CPU缓存），操作完成后再把结果从线程本地刷到主存。

**区别**

synchronized关键字解决的是执行控制的问题，它会阻止其它线程获取当前对象的监控锁，这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问，也就无法并发执行。

synchronized也可以解决内存可见性的问题:synchronized还会创建一个内存屏障，内存屏障指令保证了所有CPU操作结果都会直接刷到主存中，从而保证了操作的内存可见性，同时也使得先获得这个锁的线程的所有操作，都happens-before于随后获得这个锁的线程的操作。

volatile关键字解决的是内存可见性的问题，会使得所有对volatile变量的读写都会直接刷到主存，即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性，但需要特别注意， volatile不能保证复合操作的原子性，即使只是i++，实际上也是由多个原子操作组成：read i; inc; write i，假如多个线程同时执行i++，volatile只能保证他们操作的i是同一块内存，但依然可能出现写入脏数据的情况。

volatile本质是在告诉jvm当前变量在寄存器（工作内存）中的值是不确定的，需要从主存中读取； synchronized则是锁定当前变量，只有当前线程可以访问该变量，其他线程被阻塞住。

volatile仅能使用在变量级别；synchronized则可以使用在变量、方法、和类级别的

volatile仅能实现变量的修改可见性，不能保证原子性；而synchronized则可以保证变量的修改可见性和原子性

volatile不会造成线程的阻塞；synchronized可能会造成线程的阻塞。

volatile标记的变量不会被编译器优化；synchronized标记的变量可以被编译器优化

### 3. 有序性

有序性是指：在本线程内观察，所有操作都是有序的。在一个线程观察另一个线程，所有操作都是无序的，无序是因为发生了指令重排序。在 Java 内存模型中，允许编译器和处理器对指令进行重排序，重排序过程不会影响到单线程程序的执行，却会影响到多线程并发执行的正确性。

volatile 关键字通过添加内存屏障的方式来禁止指令重排，即重排序时不能把后面的指令放到内存屏障之前。

也可以通过 synchronized 来保证有序性，它保证每个时刻只有一个线程执行同步代码，相当于是让线程顺序执行同步代码。

## 先行发生原则

上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外，JVM 还规定了先行发生原则，让一个操作无需控制就能先于另一个操作完成。

先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。

### 1. 单一线程原则

> Single Thread rule

在一个线程内，在程序前面的操作先行发生于后面的操作。

### 2. 管程锁定规则

> Monitor Lock Rule

一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。

### 3. volatile 变量规则

> Volatile Variable Rule

对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。

### 4. 线程启动规则

> Thread Start Rule

Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。

### 5. 线程终止规则

> Thread Termination Rule

线程中的所有操作都先行发生于对此线程的终止检测，我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

### 6. 线程中断规则

> Thread Interruption Rule

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生(意思是中断先发生,才能检测到中断事件发生)，可以通过 interrupted() 方法检测到是否有中断发生。

### 7. 对象终结规则

> Finalizer Rule

一个对象的初始化完成（构造函数执行结束）先行发生于它的 finalize() 方法的开始。

### 8. 传递性

> Transitivity

如果操作 A 先行发生于操作 B，操作 B 先行发生于操作 C，那么操作 A 先行发生于操作 C。

# 十一、线程安全

多个线程不管以何种方式访问某个类，并且在主调代码中不需要进行同步，都能表现正确的行为。

线程安全有以下几种实现方式：

## 不可变

不可变（Immutable）的对象一定是线程安全的，不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来，永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下，应当尽量使对象成为不可变，来满足线程安全。

不可变的类型：

- final 关键字修饰的基本数据类型
- String
- 枚举类型
- Number 部分子类，如 Long 和 Double 等数值包装类型，BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

对于集合类型，可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

```java
public class ImmutableExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
        unmodifiableMap.put("a", 1);
    }
}
```

```html
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
    at ImmutableExample.main(ImmutableExample.java:9)
```

Collections.unmodifiableXXX() 先对原始的集合进行拷贝，需要对集合进行修改的方法都直接抛出异常。

```java
public V put(K key, V value) {
    throw new UnsupportedOperationException();
}
```

## 悲观锁(阻塞同步)

synchronized 和 ReentrantLock。

### synchronized的底层原理

synchronized的底层实现主要依靠 Lock-Free 的队列，基本思路是 自旋后阻塞，竞争切换后继续竞争锁，稍微牺牲了公平性，但获得了高吞吐量。在线程冲突较少的情况下，可以获得和CAS类似的性能；而线程冲突严重的情况下，性能远高于CAS。

### synchronized悲观锁的使用场景

多写的情况，一般会经常产生冲突，这就会导致上层应用会不断的进行retry，这样反倒是降低了性能，所以一般多写的场景下用悲观锁就比较合适。

对于资源竞争严重（线程冲突严重）的情况，CAS自旋的概率会比较大，从而浪费更多的CPU资源，效率低于synchronized。

## 乐观锁(非阻塞同步)

互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题，因此这种同步也称为阻塞同步。

互斥同步属于一种悲观的并发策略，总是认为只要不去做正确的同步措施，那就肯定会出现问题。无论共享数据是否真的会出现竞争，它都要进行加锁（这里讨论的是概念模型，实际上虚拟机会优化掉很大一部分不必要的加锁）、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

### 乐观锁的使用场景

乐观锁适用于写比较少的情况下（多读场景），即冲突真的很少发生的时候，这样可以省去了锁的开销，加大了系统的整个吞吐量。

对于资源竞争较少（线程冲突较轻）的情况，使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源；而CAS基于硬件实现，不需要进入内核，不需要切换线程，操作自旋几率较少，因此可以获得更高的性能。

## 乐观锁的两种实现方式

### CAS(compareAndSwap)

#### 定义

随着硬件指令集的发展，我们可以使用基于冲突检测的乐观并发策略：先进行操作，如果没有其它线程争用共享数据，那操作就成功了，否则采取补偿措施（不断地重试，直到成功为止）。这种乐观的并发策略的许多实现都不需要将线程阻塞，因此这种同步操作称为非阻塞同步。

乐观锁需要操作和冲突检测这两个步骤具备原子性，这里就不能再使用互斥同步来保证了，只能靠硬件来完成。硬件支持的原子性操作最典型的是：比较并交换（Compare-and-Swap，CAS）。CAS 指令需要有 3 个操作数，分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时，只有当 V 的值等于 A，才将 V 的值更新为 B。

#### 底层实现

[CAS的底层实现,二轮拓展](https://juejin.im/post/5a73cbbff265da4e807783f5)

#### ABA问题
如果一个变量V初次读取的时候是A值，并且在准备赋值的时候检查到它仍然是A值，那我们就能说明它的值没有被其他线程修改过了吗？很明显是不能的，因为在这段时间它的值可能被改为其他值，然后又改回A，那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力，其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用，并且当前标志是否等于预期标志，如果全部相等，则以原子方式将该引用和该标志的值设置为给定的更新值。

**解决办法**
针对这种情况，java并发包中提供了一个带有标记的原子引用类AtomicStampedReference，它可以通过控制变量值的版本来保证CAS的正确性。

#### 循环时间开销大

自旋CAS（也就是不成功就一直循环执行直到成功）如果长时间不成功，会给CPU带来非常大的执行开销。

 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升，pause指令有两个作用，

 第一它可以延迟流水线执行指令（de-pipeline）,使CPU不会消耗过多的执行资源，延迟的时间取决于具体实现的版本，在一些处理器上延迟时间是零。

 第二它可以避免在退出循环的时候因内存顺序冲突（memory order violation）而引起CPU流水线被清空（CPU pipeline flush），从而提高CPU的执行效率。

####  只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效，当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始，提供了AtomicReference类来保证引用对象之间的原子性，你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

 ####  AtomicInteger

[AtomicInteger中的cas应用](https://www.jianshu.com/p/fb6e91b013cc)

J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。

以下代码使用了 AtomicInteger 执行了自增的操作。
```java
public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //value的值为volatile,任何线程修改都可以被其他线程立马发现
    private volatile int value;
    public final int get() {return value;}
}

....
//原子Integer的自增算法调用了unsafe的getAndAddInt方法
public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

```

以下代码是 getAndAddInt() 源码，var1 指示对象内存地址，var2 指示该字段相对对象内存地址的偏移，var4 指示操作需要加的数值，这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值，通过调用 compareAndSwapInt() 来进行 CAS 比较，如果该字段内存地址中的值等于 var5，那么就更新内存地址为 var1+var2 的变量为 var5+var4。

如果不等于var5,代表其他线程修改了,由于AutomaticInteger的value是volatile修饰的,所以可以在do while循环里重新获取var5

```java
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
```
compareAndSwapInt是一个本地方法,在unsafe.cpp里面,

```java
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
```

```cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
```
### 版本号机制(解决ABA问题)

一般是在数据表中加上一个数据版本号version字段，表示数据被修改的次数，当数据被修改时，version值会加一。当线程A要更新数据值时，在读取数据的同时也会读取version值，在提交更新时，若刚才读取到的version值为当前数据库中的version值相等时才更新，否则重试更新操作，直到更新成功。
举一个简单的例子：
假设数据库中帐户信息表中有一个 version 字段，当前值为 1 ；而当前帐户余额字段（ balance ）为 $100 。

操作员 A 此时将其读出（ version=1 ），并从其帐户余额中扣除 $50（ $100-$50 ）。
在操作员 A 操作的过程中，操作员B 也读入此用户信息（ version=1 ），并从其帐户余额中扣除 $20 （ $100-$20 ）。
操作员 A 完成了修改工作，将数据版本号加一（ version=2 ），连同帐户扣除后余额（ balance=$50 ），提交至数据库更新，此时由于提交数据版本大于数据库记录当前版本，数据被更新，数据库记录 version 更新为 2 。
操作员 B 完成了操作，也将版本号加一（ version=2 ）试图向数据库提交数据（ balance=$80 ），但此时比对数据库记录版本时发现，操作员 B 提交的数据版本号为 2 ，数据库记录当前版本也为 2 ，不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略，因此，操作员 B 的提交被驳回。

这样，就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

## 无同步方案

要保证线程安全，并不是一定就要进行同步。如果一个方法本来就不涉及共享数据，那它自然就无须任何同步措施去保证正确性。

### 1. 栈封闭

多个线程访问同一个方法的局部变量时，不会出现线程安全问题，因为局部变量存储在虚拟机栈中，属于线程私有的。

```java
public class StackClosedExample {
    public void add100() {
        int cnt = 0;
        for (int i = 0; i < 100; i++) {
            cnt++;
        }
        System.out.println(cnt);
    }
}
```

```java
public static void main(String[] args) {
    StackClosedExample example = new StackClosedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> example.add100());
    executorService.execute(() -> example.add100());
    executorService.shutdown();
}
```

```html
100
100
```

### 2. 线程本地存储（Thread Local Storage）

如果一段代码中所需要的数据必须与其他代码共享，那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证，我们就可以把共享数据的可见范围限制在同一个线程之内，这样，无须同步也能保证线程之间不出现数据争用的问题。

　　最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。每一个线程都获得一个连接.符合这种特点的应用并不少见，大部分使用消费队列的架构模式（如“生产者-消费者”模式）都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”（Thread-per-Request）的处理方式，这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。

可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

对于以下代码，thread1 中设置 threadLocal 为 1，而 thread2 设置 threadLocal 为 2。过了一段时间之后，thread1 读取 threadLocal 依然是 1，不受 thread2 的影响。

```java
public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
            threadLocal.remove();
        });
        Thread thread2 = new Thread(() -> {
            threadLocal.set(2);
            threadLocal.remove();
        });
        thread1.start();
        thread2.start();
    }
}
```

```html
1
```

为了理解 ThreadLocal，先看以下代码：

```java
public class ThreadLocalExample1 {
    public static void main(String[] args) {
        ThreadLocal threadLocal1 = new ThreadLocal();
        ThreadLocal threadLocal2 = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal1.set(1);
            threadLocal2.set(1);
        });
        Thread thread2 = new Thread(() -> {
            threadLocal1.set(2);
            threadLocal2.set(2);
        });
        thread1.start();
        thread2.start();
    }
}
```
```java

它所对应的底层结构图为：

<div align="center"> <img src="pics/6782674c-1bfe-4879-af39-e9d722a95d39.png" width="500px"> </div><br>

每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。

```java
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
```

当调用一个 ThreadLocal 的 set(T value) 方法时，先得到当前线程的 ThreadLocalMap 对象，然后将 ThreadLocal->value 键值对插入到该 Map 中。

```java
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
```

get() 方法类似。

```java
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
```

ThreadLocal 从理论上讲并不是用来解决多线程并发问题的，因为根本不存在多线程竞争。

在一些场景 (尤其是使用线程池) 下，由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况，应该尽可能在每次使用 ThreadLocal 后手动调用 remove()，以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。

### 3. 可重入代码（Reentrant Code）

这种代码也叫做纯代码（Pure Code），可以在代码执行的任何时刻中断它，转而去执行另外一段代码（包括递归调用它本身），而在控制权返回后，原来的程序不会出现任何错误。

可重入代码有一些共同的特征，例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

# 十二、锁优化

这里的锁优化主要是指 JVM 对 synchronized 的优化。

## 阻塞的代价

要阻塞或唤醒一个线程就需要操作系统介入，需要在户态与核心态之间切换，这种切换会消耗大量的系统资源，

因为用户态与内核态都有各自专用的内存空间，专用的寄存器等，

用户态切换至内核态需要传递给许多变量、参数给内核，

内核也需要保护好用户态在切换时的一些寄存器值、变量等，以便内核态调用结束后切换回用户态继续工作。

## 自旋锁
轻量锁,自旋锁,偏向锁都是乐观锁

互斥同步进入阻塞状态的开销都很大，应该尽量避免。在许多应用中，共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环（自旋）一段时间，如果在这段时间内能获得锁，就可以避免进入阻塞状态。

自旋锁虽然能避免进入阻塞状态从而减少开销，但是它需要进行忙循环操作占用 CPU 时间，它只适用于共享数据的锁定状态很短的场景。

在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了，而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。

## 锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。

锁消除主要是通过逃逸分析来支持，如果堆上的共享数据不可能逃逸出去被其它线程访问到，那么就可以把它们当成私有数据对待，也就可以将它们的锁进行消除。

对于一些看起来没有加锁的代码，其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁：

```java
public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}
```

String 是一个不可变的类，编译器会对 String 的拼接自动优化。在 JDK 1.5 之前，会转化为 StringBuffer 对象的连续 append() 操作：

```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();
}
```

每个 append() 方法中都有一个同步块。虚拟机观察变量 sb，很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说，sb 的所有引用永远不会逃逸到 concatString() 方法之外，其他线程无法访问到它，因此可以进行消除。

## 锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁，频繁的加锁操作就会导致性能损耗。

上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁，将会把加锁的范围扩展（粗化）到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后，这样只需要加锁一次就可以了。

## 轻量级锁

JDK 1.6 引入了偏向锁和轻量级锁，从而让锁拥有了四个状态：无锁状态（unlocked）、偏向锁状态（biasble）、轻量级锁状态（lightweight locked）和重量级锁状态（inflated）。

以下是 HotSpot 虚拟机对象头的内存布局，这些数据被称为 Mark Word。其中 tag bits 对应了五个状态，这些状态在右侧的 state 表格中给出。除了 marked for gc 状态，其它四个状态已经在前面介绍过了。

<div align="center"> <img src="pics/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png" width="500"/> </div><br>

下图左侧是一个线程的虚拟机栈，其中有一部分称为 Lock Record 的区域，这是在轻量级锁运行过程创建的，用于存放锁对象的 Mark Word。而右侧就是一个锁对象，包含了 Mark Word 和其它信息。

<div align="center"> <img src="pics/051e436c-0e46-4c59-8f67-52d89d656182.png" width="500"/> </div><br>

轻量级锁是相对于传统的重量级锁而言，它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁，在整个同步周期内都是不存在竞争的，因此也就不需要都使用互斥量进行同步，可以先采用 CAS 操作进行同步，如果 CAS 失败了再改用互斥量进行同步。

当尝试获取一个锁对象时，如果锁对象标记为 0 01，说明锁对象的锁未锁定（unlocked）状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record，然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了，那么线程就获取了该对象上的锁，并且对象的 Mark Word 的锁标记变为 00，表示该对象处于轻量级锁状态。

<div align="center"> <img src="pics/baaa681f-7c52-4198-a5ae-303b9386cf47.png" width="400"/> </div><br>

如果 CAS 操作失败了，虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈，如果是的话说明当前线程已经拥有了这个锁对象，那就可以直接进入同步块继续执行，否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁，那轻量级锁就不再有效，要膨胀为重量级锁。

## 偏向锁

偏向锁的思想是偏向于让第一个获取锁对象的线程，这个线程在之后获取该锁就不再需要进行同步操作，甚至连 CAS 操作也不再需要。

当锁对象第一次被线程获得的时候，进入偏向状态，标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中，如果 CAS 操作成功，这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。

当有另外一个线程去尝试获取这个锁对象时，偏向状态就宣告结束，此时撤销偏向（Revoke Bias）后恢复到未锁定状态或者轻量级锁状态。

<div align="center"> <img src="pics/390c913b-5f31-444f-bbdb-2b88b688e7ce.jpg" width="600"/> </div><br>

# 线程运行的安全性
当多个线程要共享一个实例对象的值得时候，那么在考虑安全的多线程并发编程时就要保证下面3个要素：

原子性（Synchronized, Lock）

有序性(Volatile，Synchronized, Lock)

可见性(Volatile，Synchronized,Lock)

**原子性：** 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断，要么就都不执行。

在Java中，基本数据类型的变量的读取和赋值操作是原子性操作，即这些操作是不可被中断的，要么执行，要么不执行。

**可见性：** 指当多个线程访问同一个变量时，一个线程修改了这个变量的值，其他线程能够立即看得到修改的值。

当一个共享变量被volatile修饰时，它会保证修改的值会立即被更新到主存，当有其他线程需要读取共享变量时，它会去内存中读取新值。

普通的共享变量不能保证可见性，因为普通共享变量被修改后，什么时候被写入主存是不确定的，当其他线程去读取时，此时内存中可能还是原来的旧值，因此无法保证可见性。

更新主存的步骤：当前线程将其他线程的工作内存中的缓存变量的缓存行设置为无效，然后当前线程将变量的值跟新到主存，更新成功后将其他线程的缓存行更新为新的主存地

其他线程读取变量时，发现自己的缓存行无效，它会等待缓存行对应的主存地址被更新之后，然后去对应的主存读取最新的值。

**有序性：** 即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中，允许编译器和处理器对指令进行重排序，但是重排序过程不会影响到单线程程序的执行，却会影响到多线程并发执行的正确性。

可以通过volatile关键字来保证一定的“有序性”。

# volatile

## 内存屏障

## volatile与有序性

volatile一个强大的功能，那就是他可以禁止指令重排优化。通过禁止指令重排优化，就可以保证代码程序会严格按照代码的先后顺序执行。

<div align="center"> <img src=" .\pictures\java-concurrency\Snipaste_2019-09-25_23-24-36.jpg" width="600px"> </div><br>

当第二个操作是volatile写时，不管第一个操作是什么，都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

当第一个操作是volatile读时，不管第二个操作是什么，都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

当第一个操作是volatile写，第二个操作是volatile读时，不能重排序。

### 有序性实现原理
具体实现方式是在编译期生成字节码时，会在指令序列中增加内存屏障来保证，下面是基于保守策略的JMM内存屏障插入策略：

在每个volatile写操作的前面插入一个StoreStore屏障。
对于这样的语句Store1; StoreStore; Store2，在Store2及后续写入操作执行前，保证Store1的写入操作对其它处理器可见。

在每个volatile写操作的后面插入一个StoreLoad屏障。
对于这样的语句Store1; StoreLoad; Load2，在Load2及后续所有读取操作执行前，保证Store1的写入对所有处理器可见。

在每个volatile读操作的后面插入一个LoadLoad屏障。
对于这样的语句Load1; LoadLoad; Load2，在Load2及后续读取操作要读取的数据被访问前，保证Load1要读取的数据被读取完毕。

在每个volatile读操作的后面插入一个LoadStore屏障。
对于这样的语句Load1; LoadStore; Store2，在Store2及后续写入操作被刷出前，保证Load1要读取的数据被读取完毕。

<div align="center"> <img src=".\pictures\java-concurrency\Snipaste_2019-09-25_23-26-35.jpg " width="600px"> </div><br>

## volatile与内存可见性

volatile关键字提供了一个功能，那就是被其修饰的变量在被修改后可以立即同步到主内存，被其修饰的变量在每次是用之前都从主内存刷新。

volatile对于可见性的实现，内存屏障也起着至关重要的作用。因为内存屏障相当于一个数据同步点，他要保证在这个同步点之后的读写操作必须在这个点之前的读写操作都执行完之后才可以执行。并且在遇到内存屏障的时候，缓存数据会和主存进行同步，或者把缓存数据写入主存、或者从主存把数据读取到缓存。
### 内存模型中的缓存一致性协议

http://www.hollischuang.com/archives/2662

longlongago,多个cpu,一根总线,多级缓存(L1,L02)与内存之间的一致性,是通过在总线上加锁实现的,这样会阻塞住其他cpu,效率低下;后来就有了缓存一致性协议

### 操作系统内存中的缓存一致性与jvm中的内存一致性

已经有了缓存一致性协议，为什么Java还需要volatile？

1、并不是所有的硬件架构都提供了相同的一致性保证，Java作为一门跨平台语言，JVM需要提供一个统一的语义。

2、操作系统中的缓存和JVM中线程的本地内存并不是一回事，通常我们可以认为：MESI可以解决缓存层面的可见性问题。使用volatile关键字，可以解决JVM层面的可见性问题。

3、缓存可见性问题的延伸：由于传统的MESI协议的执行成本比较大。所以CPU通过Store Buffer和Invalidate Queue组件来解决，但是由于这两个组件的引入，也导致缓存和主存之间的通信并不是实时的。也就是说，缓存一致性模型只能保证缓存变更可以保证其他缓存也跟着改变，但是不能保证立刻、马上执行。

其实，在计算机内存模型中，也是使用内存屏障来解决缓存的可见性问题的（再次强调：缓存可见性和并发编程中的可见性可以互相类比，但是他们并不是一回事儿）。写内存屏障（Store Memory Barrier）可以促使处理器将当前store buffer（存储缓存）的值写回主存。读内存屏障（Load Memory Barrier）可以促使处理器处理invalidate queue（失效队列）。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。

## volatile与原子性
synchronized为了保证原子性，需要通过字节码指令monitorenter和monitorexit，但是volatile和这两个指令之间是没有任何关系的。volatile是不能保证原子性的。

为什么synchronized可以保证原子性 ，因为被synchronized修饰的代码片段，在进入之前加了锁，只要他没执行完，其他线程是无法获得锁执行这段代码片段的，就可以保证他内部的代码可以全部被执行。进而保证原子性。

但是synchronized对原子性保证也不绝对，如果真要较真的话，一旦代码运行异常，也没办法回滚。所以呢，在并发编程中，原子性的定义不应该和事务中的原子性一样。他应该定义为：一段代码，或者一个变量的操作，在没有执行完之前，不能被其他线程执行。

那么，为什么volatile不能保证原子性呢？因为他不是锁，他没做任何可以保证原子性的处理。当然就不能保证原子性了。

### 原子性的两种认知
可回滚(发生异常依然保持原子性)
不可回滚,但不可剥夺(发生异常失去原子性,没有异常时保持原子性)

redis的原子性是不可回滚类型,mysql是可回滚类型.synchronize是不可回滚类型

# 十三、多线程开发良好的实践

- 给线程起个有意义的名字，这样可以方便找 Bug。

- 缩小同步范围，从而减少锁争用。例如对于 synchronized，应该尽量使用同步块而不是同步方法。

- 多用同步工具少用 wait() 和 notify()。首先，CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作，而用 wait() 和 notify() 很难实现复杂控制流；其次，这些同步类是由最好的企业编写和维护，在后续的 JDK 中还会不断优化和完善。

- 使用 BlockingQueue 实现生产者消费者问题。

- 多用并发集合少用同步集合，例如应该使用 ConcurrentHashMap 而不是 Hashtable。

- 使用本地变量和不可变类来保证线程安全。

- 使用线程池而不是直接创建线程，这是因为创建线程代价很高，线程池可以有效地利用有限的线程来启动任务。

# 其他

## 线程的并发与同步

同步:
即当有一个线程在对内存进行操作时，其他线程都不可以对这个内存地址进行操作，直到该线程完成操作， 其他线程才能对该内存地址进行操作，而其他线程又处于等待状态，实现线程同步的方法有很多，临界区对象就是其中一种。

并发:
多个线程在一定的时间段内交替执行(与并行区分)

## 回调

用于异步非阻塞的情况,实现callable接口里的call方法.比如:线程a告知b要做什么任务,之后a继续做自己的事情,b完成之后会通知a.而不是a要时刻监视b是否完成.
举例:
老师类
```java
    //老师类实例化回调接口，即学生写完题目之后通过老师的提供的方法进行回调。
    //那么学生如何调用到老师的方法呢，只要在学生类的方法中传入老师的引用即可。
    //而老师需要指定学生答题，所以也要传入学生的实例。
public class Teacher implements CallBack{
    private Student student;

    Teacher(Student student) {
        this.student = student;
    }

    void askProblem (Student student, Teacher teacher) {
        //main方法是主线程运行，为了实现异步回调，这里开启一个线程来操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                student.resolveProblem(teacher);
            }
        }).start();
        //老师让学生做题以后，等待学生回答的这段时间，可以做别的事，比如玩手机.\
        //而不需要同步等待，这就是回调的好处。
        //当然你可以说开启一个线程让学生做题就行了，但是这样无法让学生通知老师。
        //需要另外的机制去实现通知过程。
        // 当然，多线程中的future和callable也可以实现数据获取的功能。
        for (int i = 1;i < 4;i ++) {
            System.out.println("等学生回答问题的时候老师玩了 " + i + "秒的手机");
        }
    }

    @Override
    public void tellAnswer(int res) {
        System.out.println("the answer is " + res);
    }
}

```
学生接口
```java
    //学生的接口，解决问题的方法中要传入老师的引用，否则无法完成对具体实例的回调。
    //写为接口的好处就是，很多个学生都可以实现这个接口，并且老师在提问题时可以通过
    //传入List<Student>来聚合学生，十分方便。
public interface Student {
    void resolveProblem (Teacher teacher);
}

```
学生tom
```java
public class Tom implements Student{

    @Override
    public void resolveProblem(Teacher teacher) {
        try {
            //学生思考了3秒后得到了答案，通过老师提供的回调方法告诉老师。
            Thread.sleep(3000);
            System.out.println("work out");
            teacher.tellAnswer(111);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

```
测试类
```java
public class Test {
    public static void main(String[] args) {
        //测试
        Student tom = new Tom();
        Teacher lee = new Teacher(tom);
        lee.askProblem(tom, lee);
        //结果
//        等学生回答问题的时候老师玩了 1秒的手机
//        等学生回答问题的时候老师玩了 2秒的手机
//        等学生回答问题的时候老师玩了 3秒的手机
//        work out
//        the answer is 111
    }
}
```

# 并发编程题

开n个线程,循环打印1~100的数字.

```java

public class Main {
    static int count = 1;
    public static void main(String[] args) throws InterruptedException {
        int threadnNum = 3;

        Thread[] threads = new Thread[threadnNum];
        Semaphore[] semaphores = new Semaphore[threadnNum];
        for (int i = 0; i < threadnNum; i++) {
            semaphores[i] = new Semaphore(1);
            semaphores[i].acquire();
        }
        semaphores[threadnNum - 1].release();
        for (int i = 0; i < threadnNum; i++) {
            final int index = i;
            final Semaphore preSemph = i == 0 ? semaphores[threadnNum - 1] : semaphores[i - 1];
            final Semaphore curSemph = semaphores[i];
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            preSemph.acquire();
                            if (count > 20) {
                                System.exit(0);
                            }
                            System.out.println("index:" + index + ",count:" + count++);
                            curSemph.release();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }
    }
}
```

<file_sep>/notes/Hadoop.md
# Hadoop

# Yarn

# HDFS

# MapReduce

## 优缺点

### 优点

1Mapreduce 易于编程

它简单的实现一些接口，就可以完成个分布式程序，这个分布式程序可以分布到大量廉价的 PC 机器上运行。也就是说你写一个分布式程序，跟写个简单的串行程序是一模一样的。就是因为这个特点使得 Map？编程变得非常流行。

2 良好的扩展性

当你的计算资源不能得到满足的时候，你可以通过简单的增加机器来扩展它的计算能力。

3. 高容错性

Mapreduce 设计的初衷就是使程序能够部署在廉价的 PC 机器上，这就要求它具有很高的容错性。比如其中一台机器挂了，它可以把上面的计算任务转移到另外一个节点上运行，不至于这个任务运行失败，而且这个过程不需要人工参与，而完全是由 Hadoop F 内部完成的

4 适合 PB 级以上海量数据的离线处理

可以实现上干台服务器集群并发工作，提供数据处理能力。

### 缺点

1. 不擅长实时计算，无法在毫秒内返回结果
2. 不擅长流式计算
3. 不擅长有向图计算

## 核心思想

![image-20200430162523026](pictures/Hadoop/image-20200430162523026.png)

1）分布式的运算程序往往需要分成至少2个阶段。

2）第一个阶段的MapTask并发实例，完全并行运行，互不相干。

3）第二个阶段的ReduceTask并发实例互不相干，但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。

4）MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段，如果用户的业务逻辑非常复杂，那就只能多个MapReduce程序，串行运行。

总结：分析WordCount数据流走向深入理解MapReduce核心思想。

## MapReduce进程

<img src="pictures/Hadoop/image-20200430163357832.png" alt="image-20200430163357832" style="zoom:50%;" />

<img src="pictures/Hadoop/image-20200430164827868.png" alt="image-20200430164827868" style="zoom:50%;" />

<img src="pictures/Hadoop/image-20200430165007091.png" alt="image-20200430165007091" style="zoom:50%;" />

## 统计文件中单词出现的个数

![image-20200430165535778](pictures/Hadoop/image-20200430165535778.png)

### 编写mapper

```java
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{

	Text k = new Text();
	IntWritable v = new IntWritable(1);

	@Override
	protected void map(LongWritable key, Text value, Context context)	throws IOException, InterruptedException {

		// 1 获取一行
		String line = value.toString();

		// 2 切割
		String[] words = line.split(" ");

		// 3 输出
		for (String word : words) {

			k.set(word);
			context.write(k, v);
		}
	}
}

```

### Reducer

```java
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{

int sum;
IntWritable v = new IntWritable();

	@Override
	protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {

		// 1 累加求和
		sum = 0;
		for (IntWritable count : values) {
			sum += count.get();
		}

		// 2 输出
       v.set(sum);
		context.write(key,v);
	}
}

```

### Driver

```java
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class WordcountDriver {

	public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

		// 1 获取配置信息以及封装任务
		Configuration configuration = new Configuration();
		Job job = Job.getInstance(configuration);

		// 2 设置jar加载路径
		job.setJarByClass(WordcountDriver.class);

		// 3 设置map和reduce类
		job.setMapperClass(WordcountMapper.class);
		job.setReducerClass(WordcountReducer.class);

		// 4 设置map输出
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(IntWritable.class);

		// 5 设置最终输出kv类型
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);

		// 6 设置输入和输出路径
		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));

		// 7 提交
		boolean result = job.waitForCompletion(true);

		System.exit(result ? 0 : 1);
	}
}

```

## 序列化与反序列化(制作bean)

注意反序列化的顺序和序列化的顺序完全一致

可以方便数据组织成一个对象在网络中传输

### 适用场景

<img src="pictures/Hadoop/image-20200430174827149.png" alt="image-20200430174827149" style="zoom:50%;" />

### Bean

```java
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Writable;

// 1 实现writable接口
public class FlowBean implements Writable{

	private long upFlow;
	private long downFlow;
	private long sumFlow;

	//2  反序列化时，需要反射调用空参构造函数，所以必须有
	public FlowBean() {
		super();
	}

	public FlowBean(long upFlow, long downFlow) {
		super();
		this.upFlow = upFlow;
		this.downFlow = downFlow;
		this.sumFlow = upFlow + downFlow;
	}

	//3  写序列化方法
	@Override
	public void write(DataOutput out) throws IOException {
		out.writeLong(upFlow);
		out.writeLong(downFlow);
		out.writeLong(sumFlow);
	}

	//4 反序列化方法
	//5 反序列化方法读顺序必须和写序列化方法的写顺序必须一致
	@Override
	public void readFields(DataInput in) throws IOException {
		this.upFlow  = in.readLong();
		this.downFlow = in.readLong();
		this.sumFlow = in.readLong();
	}

	// 6 编写toString方法，方便后续打印到文本
	@Override
	public String toString() {
		return upFlow + "\t" + downFlow + "\t" + sumFlow;
	}

	public long getUpFlow() {
		return upFlow;
	}

	public void setUpFlow(long upFlow) {
		this.upFlow = upFlow;
	}

	public long getDownFlow() {
		return downFlow;
	}

	public void setDownFlow(long downFlow) {
		this.downFlow = downFlow;
	}

	public long getSumFlow() {
		return sumFlow;
	}

	public void setSumFlow(long sumFlow) {
		this.sumFlow = sumFlow;
	}
}

```

### Mapper

```java
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class FlowCountMapper extends Mapper<LongWritable, Text, Text, FlowBean>{

	FlowBean v = new FlowBean();
	Text k = new Text();

	@Override
	protected void map(LongWritable key, Text value, Context context)	throws IOException, InterruptedException {

		// 1 获取一行
		String line = value.toString();

		// 2 切割字段
		String[] fields = line.split("\t");

		// 3 封装对象
		// 取出手机号码
		String phoneNum = fields[1];

		// 取出上行流量和下行流量
		long upFlow = Long.parseLong(fields[fields.length - 3]);
		long downFlow = Long.parseLong(fields[fields.length - 2]);

		k.set(phoneNum);
		v.set(downFlow, upFlow);

		// 4 写出
		context.write(k, v);
	}
}

```

### Reducer

```java
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class FlowCountReducer extends Reducer<Text, FlowBean, Text, FlowBean> {

	@Override
	protected void reduce(Text key, Iterable<FlowBean> values, Context context)throws IOException, InterruptedException {

		long sum_upFlow = 0;
		long sum_downFlow = 0;

		// 1 遍历所用bean，将其中的上行流量，下行流量分别累加
		for (FlowBean flowBean : values) {
			sum_upFlow += flowBean.getUpFlow();
			sum_downFlow += flowBean.getDownFlow();
		}

		// 2 封装对象
		FlowBean resultBean = new FlowBean(sum_upFlow, sum_downFlow);

		// 3 写出
		context.write(key, resultBean);
	}
}

```

### Driver

```java
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class FlowsumDriver {

	public static void main(String[] args) throws IllegalArgumentException, IOException, ClassNotFoundException, InterruptedException {

// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[] { "e:/input/inputflow", "e:/output1" };

		// 1 获取配置信息，或者job对象实例
		Configuration configuration = new Configuration();
		Job job = Job.getInstance(configuration);

		// 6 指定本程序的jar包所在的本地路径
		job.setJarByClass(FlowsumDriver.class);

		// 2 指定本业务job要使用的mapper/Reducer业务类
		job.setMapperClass(FlowCountMapper.class);
		job.setReducerClass(FlowCountReducer.class);

		// 3 指定mapper输出数据的kv类型
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(FlowBean.class);

		// 4 指定最终输出的数据的kv类型
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(FlowBean.class);

		// 5 指定job的输入原始文件所在目录
		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));

		// 7 将job中配置的相关参数，以及job所用的java类所在的jar包， 提交给yarn去运行
		boolean result = job.waitForCompletion(true);
		System.exit(result ? 0 : 1);
	}
}

```

##

## MapTask并行度决定机制

**数据块：**Block是HDFS物理上把数据分成一块一块。

**数据切片：**数据切片只是在逻辑上对输入进行分片，并不会在磁盘上将其切分成片进行存储。

![image-20200430205958234](pictures/Hadoop/image-20200430205958234.png)

## 切片

![image-20200430222342942](pictures/Hadoop/image-20200430222342942.png)

###  CombineTextInputFormat切片机制

框架默认的TextInputFormat切片机制是对任务按文件规划切片，不管文件多小，都会是一个单独的切片，都会交给一个MapTask，这样如果有大量小文件，就会产生大量的MapTask，处理效率极其低下。

1、应用场景：

CombineTextInputFormat用于小文件过多的场景，它可以将多个小文件从逻辑上规划到一个切片中，这样，多个小文件就可以交给一个MapTask处理。

2、虚拟存储切片最大值设置

CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m

注意：虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。

3、切片机制

生成切片过程包括：虚拟存储过程和切片过程二部分。

<file_sep>/codes/javacode/designpattern/Singleton4.java
package javacode.designpattern;

import java.util.Arrays;

/**
 * @author <EMAIL>
 * @date 19/9/16/016 8:25
 */
@SuppressWarnings("all")
public class Singleton4 {

    private void quickSort(int[] nums, int low, int high) {
        if (low > high) {
            return;
        }
        int loc = partition(nums, low, high);
        quickSort(nums, low, loc - 1);
        quickSort(nums, loc + 1, high);
    }

    private int partition(int[] nums, int low, int high) {
        int temp = nums[low];
        while (low < high) {
            while (low < high && nums[high] >= temp) {
                high--;
            }
            nums[low] = nums[high];
            while (low < high && nums[low] <= temp) {
                low++;
            }
            nums[high] = nums[low];

        }
        nums[low]=temp;
        return  low;
    }

    public static void main(String[] args) {
        int[] array = {1, 5, 23, 5, 8, 9, 34, 7, 5,0};
        new Singleton4().quickSort(array, 0, array.length - 1);
        System.out.println(Arrays.toString(array));

    }
}
<file_sep>/notes/redis.md
<!-- TOC -->

- [1. 概述](#1-概述)
- [2. 数据类型](#2-数据类型)
    - [2.1. STRING](#21-string)
    - [2.2. LIST](#22-list)
    - [2.3. SET](#23-set)
    - [2.4. HASH](#24-hash)
    - [2.5. ZSET](#25-zset)
    - [2.6. 五种基本类型的底层实现TODO](#26-五种基本类型的底层实现todo)
- [3. 数据结构](#3-数据结构)
    - [3.1. 字典](#31-字典)
    - [3.2. 跳跃表](#32-跳跃表)
- [4. 使用场景](#4-使用场景)
    - [4.1. 计数器](#41-计数器)
    - [4.2. 缓存](#42-缓存)
    - [4.3. 查找表](#43-查找表)
    - [4.4. 消息队列](#44-消息队列)
    - [4.5. 会话缓存](#45-会话缓存)
    - [4.6. 分布式锁实现](#46-分布式锁实现)
    - [4.7. 其它](#47-其它)
- [5. Redis 与 Memcached](#5-redis-与-memcached)
    - [5.1. 数据类型](#51-数据类型)
    - [5.2. 数据持久化](#52-数据持久化)
    - [5.3. 分布式](#53-分布式)
    - [5.4. 内存管理机制](#54-内存管理机制)
    - [5.5. 缓存过期机制](#55-缓存过期机制)
- [6. 数据淘汰策略(如何保证Redis中的数据都是热点数据)](#6-数据淘汰策略如何保证redis中的数据都是热点数据)
    - [Redis 具体有 6 种淘汰策略：](#redis-具体有-6-种淘汰策略)
    - [Redis过期key清除策略](#redis过期key清除策略)
- [Redis过期细节](#redis过期细节)
    - [清除过期时间](#清除过期时间)
    - [Redis键值2种失效方式](#redis键值2种失效方式)
- [7. 持久化](#7-持久化)
    - [7.1. RDB 持久化](#71-rdb-持久化)
        - [rdb 的bgsave 很占用内存](#rdb-的bgsave-很占用内存)
        - [rdb的原理](#rdb的原理)
        - [写时复制的原理](#写时复制的原理)
    - [7.2. AOF 持久化](#72-aof-持久化)
    - [AOF 重写](#aof-重写)
    - [7.3. Redis两种持久化方案对比](#73-redis两种持久化方案对比)
- [8. 事务](#8-事务)
    - [8.1. redis事务相关命令](#81-redis事务相关命令)
    - [8.2. redis数据库事务的ACID与传统关系型事务的比较](#82-redis数据库事务的acid与传统关系型事务的比较)
        - [8.2.1. 原子性（Atomicity）](#821-原子性atomicity)
        - [8.2.2. 一致性（Consistency）](#822-一致性consistency)
        - [8.2.3. 隔离性（Isolation）](#823-隔离性isolation)
        - [8.2.4. 持久性（Durability）](#824-持久性durability)
    - [10.3. redis主从集群的搭建与docker通信知识](#103-redis主从集群的搭建与docker通信知识)
        - [10.3.1. docker搭建redis主从集群](#1031-docker搭建redis主从集群)
        - [10.3.2. docker 搭建redis-sentinel集群](#1032-docker-搭建redis-sentinel集群)
- [11. 订阅机制](#11-订阅机制)
    - [11.1. redis的消息订阅发布和mq对比？](#111-redis的消息订阅发布和mq对比)
- [12. redis作为二级缓存](#12-redis作为二级缓存)
    - [12.1. 实现](#121-实现)
    - [12.2. springboot cache自定义项](#122-springboot-cache自定义项)
    - [12.3. mysql与redis性能对比](#123-mysql与redis性能对比)
- [13. 缓存穿透与缓存雪崩](#13-缓存穿透与缓存雪崩)
    - [13.1. 缓存穿透](#131-缓存穿透)
    - [13.2. 缓存雪崩](#132-缓存雪崩)
    - [13.3. 缓存雪崩解决方案](#133-缓存雪崩解决方案)
- [14. redis实现分布式锁(解决不同客户端并发操作某个数据)](#14-redis实现分布式锁解决不同客户端并发操作某个数据)
    - [14.1. 分布式锁是什么](#141-分布式锁是什么)
    - [14.2. 分布锁实现方案分析](#142-分布锁实现方案分析)
        - [14.2.1. 分布式锁面试题](#1421-分布式锁面试题)
- [15. redis高可用集群](#15-redis高可用集群)
    - [15.1. 主从复制的原理](#151-主从复制的原理)
    - [15.2. 分布式系统高可用](#152-分布式系统高可用)
    - [数据迁移的三种方式](#数据迁移的三种方式)
    - [15.3. Redis高可用架构Sentinel(解决宕机)](#153-redis高可用架构sentinel解决宕机)
        - [15.3.1. Sentinel三大工作任务](#1531-sentinel三大工作任务)
        - [15.3.2. 冷备和热备](#1532-冷备和热备)
        - [15.3.3. Sentinel故障转移原理](#1533-sentinel故障转移原理)
        - [15.3.4. sentinel整合Springboot实战](#1534-sentinel整合springboot实战)
    - [15.4. Redis内置高可用集群](#154-redis内置高可用集群)
        - [15.4.1. Redis集群搭建](#1541-redis集群搭建)
        - [15.4.2. 一致性Hash算法](#1542-一致性hash算法)
            - [一致性hash的特性](#一致性hash的特性)
        - [15.4.3. 一致性Hash算法虚拟节点](#1543-一致性hash算法虚拟节点)
        - [15.4.4. twemproxy实现hash分片](#1544-twemproxy实现hash分片)
- [16. 布隆过滤器](#16-布隆过滤器)
        - [16.0.5. 介绍](#1605-介绍)
        - [16.0.6. 使用场景](#1606-使用场景)
    - [16.1. 过滤器原理](#161-过滤器原理)
    - [16.2. goole布隆过滤器与Redis布隆过滤器](#162-goole布隆过滤器与redis布隆过滤器)
    - [布隆过滤器扩容](#布隆过滤器扩容)
    - [16.3. Redis布隆过滤器安装](#163-redis布隆过滤器安装)
    - [16.4. Redis布隆过滤器与springboot的整合探索](#164-redis布隆过滤器与springboot的整合探索)
- [redis的并发竞争问题(修改一个值,商品超卖)](#redis的并发竞争问题修改一个值商品超卖)
    - [解决方案](#解决方案)
        - [分布式锁](#分布式锁)
            - [数据库实现方式](#数据库实现方式)
            - [zookeeper实现](#zookeeper实现)
            - [redis实现](#redis实现)
        - [利用消息队列](#利用消息队列)
        - [用事务或者lua脚本实现乐观锁](#用事务或者lua脚本实现乐观锁)
        - [其他想法](#其他想法)
- [redis与数据库的一致性(缓存入库万一mysql失败了怎么办)](#redis与数据库的一致性缓存入库万一mysql失败了怎么办)
    - [读缓存时](#读缓存时)
    - [写缓存时](#写缓存时)
        - [为什么是删除缓存，而不是更新缓存？](#为什么是删除缓存而不是更新缓存)
    - [高并发的场景下，该解决方案要注意的问题：](#高并发的场景下该解决方案要注意的问题)
- [RDB原理再理解](#rdb原理再理解)
    - [rdbSave](#rdbsave)
    - [save && bgsave](#save--bgsave)
    - [bgsave](#bgsave)
    - [服务器的dirty](#服务器的dirty)
    - [rdbLoad](#rdbload)
- [lua脚本保证操作原子性](#lua脚本保证操作原子性)
- [雪花算法](#雪花算法)
    - [SnowFlake作用](#snowflake作用)
    - [雪花中的溢出问题](#雪花中的溢出问题)
    - [雪花中的冬令时问题(时钟回拨问题)](#雪花中的冬令时问题时钟回拨问题)
- [面试题](#面试题)
    - [修改配置不重启Redis会实时生效吗？](#修改配置不重启redis会实时生效吗)
    - [Redis的内存用完了会发生什么？](#redis的内存用完了会发生什么)
    - [redis常见性能问题和解决方案：](#redis常见性能问题和解决方案)

<!-- /TOC -->
# 1. 概述

Redis 是速度非常快的非关系型（NoSQL）内存键值数据库，可以存储键和五种不同类型的值之间的映射。

Redis为单进程单线程模式，采用队列模式将并发访问变为串行访问

键的类型只能为字符串，值支持五种数据类型：字符串、列表、集合、散列表、有序集合。

Redis 支持很多特性，例如将内存中的数据持久化到硬盘中，使用复制来扩展读性能，使用分片来扩展写性能。

# 2. 数据类型

| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
| LIST | 列表 | 从两端压入或者弹出元素 </br> 对单个或者多个元素进行修剪，</br> 只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
| HASH(map) | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</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/)

## 2.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.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"
```

## 2.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"
```

## 2.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"
```

## 2.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"
```

## bitmap

### BitMap的命令

#### SETBIT

> 语法：SETBIT key offset value

##### 说明：

对 `key` 所储存的字符串值，设置或清除指定偏移量上的位(bit)。

位的设置或清除取决于 `value` 参数，可以是 `0` 也可以是 `1` 。

当 `key` 不存在时，自动生成一个新的字符串值。

字符串会进行伸展(grown)以确保它可以将 `value` 保存在指定的偏移量上。当字符串值进行伸展时，空白位置以 `0` 填充。

`offset` 参数必须大于或等于 `0` ，小于 2^32 (bit 映射被限制在 512 MB 之内)。

**对使用大的 `offset` 的 `SETBIT` 操作来说，内存分配可能造成 Redis 服务器被阻塞。**

##### 返回值：

字符串值指定偏移量上原来储存的位(bit)。

### 底层实现

long类型数组

### 应用

统计用户在线数

对十亿数据取top

用户签到

### 输入稀疏问题

可以把连续的0压缩成 0的数量长度

## 2.6. 五种基本类型的底层实现TODO

# 3. 数据结构

## 3.1. 字典

dictht 是一个散列表结构，使用拉链法解决哈希冲突。

```c
/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
```

```c
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
```

Redis 的字典 dict 中包含两个哈希表 dictht，这是为了方便进行 rehash 操作。在扩容时，将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面，完成之后释放空间并交换两个 dictht 的角色。

```c
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;
```

rehash 操作不是一次性完成，而是采用渐进方式，这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。

渐进式 rehash 通过记录 dict 的 rehashidx 完成，它从 0 开始，然后每执行一次 rehash 都会递增。例如在一次 rehash 中，要把 dict[0] rehash 到 dict[1]，这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上，dict[0] 的 table[rehashidx] 指向 null，并令 rehashidx++。

在 rehash 期间，每次对字典执行添加、删除、查找或者更新操作时，都会执行一次渐进式 rehash。

采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上，因此对字典的查找操作也需要到对应的 dictht 去执行。

```c
/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
    int empty_visits = n * 10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while (n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long) d->rehashidx);
        while (d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while (de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}
```

## 3.2. 跳跃表

是有序集合的底层实现之一。

跳跃表是基于多指针有序链表实现的，可以看成多个有序链表。

<div align="center"> <img src="pics/beba612e-dc5b-4fc2-869d-0b23408ac90a.png" width="600px"/> </div><br>

在查找时，从上层指针开始查找，找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。

<div align="center"> <img src="pics/0ea37ee2-c224-4c79-b895-e131c6805c40.png" width="600px"/> </div><br>

与红黑树等平衡树相比，跳跃表具有以下优点：

- 插入速度非常快速，因为不需要进行旋转等操作来维护平衡性；
- 更容易实现；
- 支持无锁操作。

# 4. 使用场景

## 4.1. 计数器

可以对 String 进行自增自减运算，从而实现计数器功能。

Redis 这种内存型数据库的读写性能非常高，很适合存储频繁读写的计数量。

## 4.2. 缓存

将热点数据放到内存中，设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

## 4.3. 查找表

例如 DNS 记录就很适合使用 Redis 进行存储。

查找表和缓存类似，也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效，而缓存的内容可以失效，因为缓存不作为可靠的数据来源。

## 4.4. 消息队列

List 是一个双向链表，可以通过 lpush 和 rpop 写入和读取消息

不过最好使用 Kafka、RabbitMQ 等消息中间件。

## 4.5. 会话缓存

可以使用 Redis 来统一存储多台应用服务器的会话信息。

当应用服务器不再存储用户的会话信息，也就不再具有状态，一个用户可以请求任意一个应用服务器，从而更容易实现高可用性以及可伸缩性。

## 4.6. 分布式锁实现

在分布式场景下，无法使用单机环境下的锁来对多个节点上的进程进行同步。

可以使用 Redis 自带的 SETNX 命令实现分布式锁，除此之外，还可以使用官方提供的 RedLock 分布式锁实现。

## 4.7. 其它

Set 可以实现交集、并集等操作，从而实现共同好友等功能。

ZSet 可以实现有序性操作，从而实现排行榜等功能。

# 5. Redis 与 Memcached

两者都是非关系型内存键值数据库，主要有以下不同：

## 5.1. 数据类型

Memcached 仅支持字符串类型的[k,v]，而 Redis 支持五种不同的数据类型，可以更灵活地解决问题。

## 5.2. 数据持久化

Redis 支持两种持久化策略：RDB 快照和 AOF 日志,rdb：属于全量数据备份，备份的是数据,aof：append only if,增量持久化备份，备份的是指令[如：set key， del key]

而 Memcached 不支持持久化。

## 5.3. 分布式

Memcached 不支持分布式，只能通过在客户端使用一致性哈希来实现分布式存储，这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。

Redis Cluster 实现了分布式的支持。

## 5.4. 内存管理机制

- 在 Redis 中，并不是所有数据都一直存储在内存中，可以将一些很久没用的 value 交换到磁盘，而 Memcached 的数据则会一直在内存中。

- Memcached 将内存分割成特定长度的块来存储数据，以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高，例如块的大小为 128 bytes，只存储 100 bytes 的数据，那么剩下的 28 bytes 就浪费掉了。而redis直接申请分配内存,会产生内存碎片.

## 5.5. 缓存过期机制

缓存数据过期机制  key，在一个小时之后过期，超过一个小时查数据就会查不到  key:"1",value:"2"

1）Memcached 在删除失效主键时也是采用的消极方法，即 Memcached 内部也不会监视主键是否失效，而是在通过 Get访问主键时才会检查其是否已经失效

2）Redis 定时、定期等多种缓存失效机制，减少内存泄漏

# 6. 数据淘汰策略(如何保证Redis中的数据都是热点数据)

可以设置内存最大使用量，当内存使用量超出时，会施行数据淘汰策略。

## Redis 具体有 6 种淘汰策略：

| 策略 | 描述 |
| :--: | :--: |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 |

**Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略，LFU 策略通过统计访问频率，将访问频率最少的键值对淘汰。**

作为内存数据库，出于对性能和内存消耗的考虑，Redis 的淘汰算法实际实现上并非针对所有 key，而是抽样一小部分并且从中选出被淘汰的 key。

使用 Redis 缓存数据时，为了提高缓存命中率，需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量，然后启用 allkeys-lru 淘汰策略，将最近最少使用的数据淘汰。

##  Redis过期key清除策略

**简介：redis过期key清除策略分析**

- Redis如何淘汰过期的keys：   set name daniel 3600

  - 惰性删除  ：

    * 概念：当一些客户端尝试访问它时，key会被发现并主动的过期

    * 放任键过期不管，但是每次从键空间中获取键时，都检查取得的键是否过期，如果过期的话，就删除该键

    * 特点：**CPU友好**，但如果一个key不再使用，那么它会一直存在于内存中，造成浪费

  - 定时删除：

    * 概念：设置键的过期时间的同时，创建一个定时器（timer），让定时器在键的过期时间来临时，立即执行对键的删除操作

  - 定期删除：

    * 隔一段时间，程序就对数据库进行一次检查，删除里面的过期键，至于要删除多少过期键，

      以及要检查多少个数据库，则由算法决定。 即设置一个定时任务，比如10分钟删除一次过期的key；间隔小则占用CPU,间隔大则浪费内存

    * 例如Redis每秒处理：

    1. 测试随机的20个keys进行相关过期检测。
    2. 删除所有已经过期的keys。
    3. 如果有多于25%的keys过期，重复步奏1.

Redis服务器实际使用的是惰性删除和定期删除两种策略：通过配合使用这两种删除策略，服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。

惰性删除策略是怎么实现？通过expireIfNeeded函数，当我们操作key的时候进行判断key是否过期

定期删除策略是怎么实现的？通过activeExpireCycle函数，serverCron函数执行时，activeExpireCycle函数就会被调用，规定的时间里面分多次遍历服务器的expires字典随机检查一部分key的过期时间，并删除其中的过期key

# Redis过期细节
## 清除过期时间
生存时间可以通过使用 DEL 命令来删除整个 key 来移除，或者被 SET 和 GETSET 命令覆盖原来的数据，

也就是说，修改key对应的value和使用另外相同的key和value来覆盖以后，当前数据的生存时间不同。

比如说，对一个 key 执行INCR命令，对一个列表进行LPUSH命令，或者对一个哈希表执行HSET命令，这类操作都不会修改 key 本身的生存时间

## Redis键值2种失效方式

被动：

当客户端尝试获取key时，发现key超时逾期了，然后删掉。
但这不够，因为有些key可能永远不会被再次访问。
这就要用到主动。

主动：

Redis每秒钟进行10次的定时检查，从要失效的key集合中随机抽取，

# 7. 持久化

Redis 是内存型数据库，为了保证数据在断电后不会丢失，需要将内存中的数据持久化到硬盘上。

## 7.1. RDB 持久化

 RDB 文件是一个压缩的**二进制文件**，通过该文件可以还原生成 RDB 文件时的数据库状态

有两个命令可以生成 RDB 文件，一个是 SAVE、另一个是 BGSAVE。

 **两者的区别**在于：前者会阻塞 Redis 服务器进程，直到 RDB 文件创建完毕为止。而在服务器进程阻塞期间，服务器是不能处理任何命令请求的。后者则不会阻塞服务器进程，因为是通过 fork 一个子进程，并让其去创建 RDB 文件，而服务器进程（父进程）继续则继续处理命令请求。

当写完数据库状态后，**新 RDB 文件就会原子地替换旧的 RDB 文件**。

对于 RDB 持久化而言，我们一般都会使用 BGSAVE 来持久化，毕竟它不会阻塞服务器进程。

在 Redis 的配置文件，有提供设置服务器每隔多久时间来执行 BGSAVE 命令。

- 在Redis中RDB持久化的触发分为两种：自己手动触发与Redis定时触发

- 而自动触发的场景主要是有以下几点

  * 根据我们的 `save m n` 配置规则自动触发
  * save 900 1 // 900 秒内，对数据库至少修改 1 次。
  * 从节点全量复制时，主节点发送rdb文件给从节点完成复制操作，主节点会触发 `bgsave`
  * 执行 `debug reload` 时
  * 执行 `shutdown`时，如果没有开启aof，也会触发

- 禁用RDB
  - 只需在save的最后一行写上：save  ""

### rdb 的bgsave 很占用内存
RDB 快照的持久化，需要注意：在进行快照的时候（save），fork 出来进行 dump 操作的子进程会占用与父进程一样的内存，真正的 copy-on-write，对性能的影响和内存的耗用都是比较大的。比如机器 8G 内存，Redis 已经使用了 6G 内存，这时 save 的话会再生成 6G，变成 12G，大于系统的 8G。这时候会发生交换；要是虚拟内存不够则会崩溃，导致数据丢失。所以在用 redis 的时候一定对系统内存做好容量规划。

### rdb的原理
父进程fork()一个子进程来进行数据备份,

**fork() 与 exec() 的区别**

当前进程调用fork()，会创建一个跟当前进程完全相同的子进程(除了pid)，所以子进程同样是会执行fork()之后的代码。

exec函数的作用就是：装载一个新的程序（可执行映像）覆盖当前进程内存空间中的映像，从而执行不同的任务。exec系列函数在执行时会直接替换掉当前进程的地址空间。

<div align="center"> <img src=".\pictures\redis\Snipaste_2019-09-25_19-55-38.jpg " width="600px"> </div><br>

但大多数情况下,我们需要的是fork()出一个子进程,但这个子进程跟父进程做不一样的事情,这个时候很多拷贝来的数据都是用不到的,所以就用到了写时复制的技术:

fork创建出的子进程，与父进程共享内存空间。也就是说，如果子进程不对内存空间进行写入操作的话，内存空间中的数据并不会复制给子进程，这样创建子进程的速度就很快了！(不用复制，直接引用父进程的物理空间)。
并且如果在fork函数返回之后，子进程第一时间exec一个新的可执行映像，那么也不会浪费时间和内存空间了

### 写时复制的原理
fork()之后，kernel把父进程中所有的内存页的权限都设为read-only，然后子进程的地址空间指向父进程。当父子进程都只读内存时，相安无事。当其中某个进程写内存时，CPU硬件检测到内存页是read-only的，于是触发页异常中断（page-fault），陷入kernel的一个中断例程。中断例程中，kernel就会把触发的异常的页复制一份，于是父子进程各自持有独立的一份。

Copy On Write技术好处是什么？

COW技术可减少分配和复制大量资源时带来的瞬间延时。
COW技术可减少不必要的资源分配。比如fork进程时，并不是所有的页面都需要复制，父进程的代码段和只读数据段都不被允许修改，所以无需复制。

Copy On Write技术缺点是什么？

如果在fork()之后，父子进程都还需要继续进行写操作，那么会产生大量的分页错误(页异常中断page-fault)，这样就得不偿失。

## 7.2. AOF 持久化

将写命令添加到 AOF 文件（Append Only File）的末尾。

使用 AOF 持久化需要设置同步选项，从而确保写命令同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上，而是先存储到缓冲区，然后由操作系统决定什么时候同步到磁盘。有以下同步选项：

| 选项 | 同步频率 |
| :--: | :--: |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |

- always 选项会严重减低服务器的性能；
- everysec 选项比较合适，可以保证系统崩溃时只会丢失一秒左右的数据，并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响；
- no 选项并不能给服务器性能带来多大的提升，而且也会增加系统崩溃时数据丢失的数量。`

随着服务器写请求的增多，AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性，能够去除 AOF 文件中的冗余写命令。

AOF 持久化功能的实现可以分为 3 个步骤：命令追加、文件写入、文件同步

命令追加很好理解，就是将写命令追加到 AOF 缓冲区的末尾。

文件写入是缓冲区内容写到 AOF 文件，文件写入是将 AOF 文件保存到磁盘。

因为服务器在处理文件事件时，可能会发生写操作，使得一些内容会被追加到 AOF 缓冲区末尾。所以，在服务器每次结束一个事件循环之前 ，都会调用 flushAppendOnlyFile 方法。

这个方法执行以下两个工作： - WRITE：根据条件，将缓冲区内容写入到 AOF 文件。 - SAVE：根据条件，调用 fsync 或 fdatasync 函数，将 AOF 文件保存到磁盘中。

两个步骤都需要根据一定的条件来执行，而这些条件由 Redis 配置文件中的 appendfsync 选项来决定的，一共有三个选择： 1. appendfsync always：每执行一个命令保存一次 2. appendfsync everysec（默认，推荐）：每一秒钟保存一次 3. appendfsync no：不保存

appendfsync always：每次执行完一个命令之后， WRITE 和 SAVE 都会被执行
appendfsync everysec：SAVE 原则上每隔一秒钟就会执行一次。
appendfsync no：每次执行完一个命令之后， WRITE 会执行，SAVE 都会被忽略，只会在以下任意一种情况中被执行：
Redis 被关闭
AOF 功能被关闭
系统的写缓存被刷新（可能是缓存已经被写满，或者定期保存操作被执行。完成依赖 OS 的写入，一般为 30 秒左右一次）

## AOF 重写

1. 保存数据库的状态差异,导致命令可以合并

2. 子进程重写过程中,父进程创建重写缓冲区,子进程重写完成在附加上重写缓冲区的内容,形成新文件.

通过该功能来创建一个新的 AOF 文件来代替旧文件。并且两个文件所保存的数据库状态一样，但新文件不会包含任何冗余命令，所以新文件要比旧文件小得多。

而为什么新文件不会包含任何冗余命令呢？

**那是因为这个重写功能是通过读取服务器当前的数据库状态来实现的。虽然叫做「重写」，但并没有对旧文件进行任何读取修改**。

比如旧文件保存了对某个 key 有 4 个 set 命令，经过重写之后，新文件只会记录**最后一次对该 key 的 set 命令**。因此说新文件不会包含任何冗余命令

因为重写涉及到大量 IO 操作，所以 Redis 是**用子进程来实现**这个功能的，否则将会阻塞主进程。该子进程拥有父进程的数据副本，可以避免在使用锁的情况下，保证数据的安全性。

那么这里又会存在一个问题，子进程在重写过程中，服务器还在继续处理命令请求，新命令可能会对数据库进行修改，这会导致当前数据库状态和重写后的 AOF 文件，所保存的数据库状态不一致。

为了解决这个问题，Redis 设置了一个 **AOF 重写缓冲区** 。在子进程执行 AOF 重写期间，主进程需要执行以下三个步骤： 1. 执行客户端的请求命令 2. 将执行后的写命令追加到 AOF 缓冲区 3. 将执行后的写命令追加到 AOF 重写缓冲区

当子进程结束重写后，会向主进程发送一个信号，主进程接收到之后会调用信号处理函数执行以下步骤： 1. 将 AOF 重写缓冲区内容写入新的 AOF 文件中。此时新文件所保存的数据库状态就和当前数据库状态一致了 2. 对新文件进行改名，原子地覆盖现有 AOF 文件，完成新旧文件的替换。

当函数执行完成后，主进程就继续处理客户端命令。

因此，在整个 AOF 重写过程中，只有在执行信号处理函数时才会阻塞主进程，其他时候都不会阻塞。

## 7.3. Redis两种持久化方案对比

**首先注意一点: Master 上 Snapshot 和 AOF 都不做，来保证 Master 的读写性能，而 Slave 上则同时开启 Snapshot 和 AOF 来进行持久化，保证数据的安全性。**

- Redis提供了不同的持久性选项：

  - RDB持久性以指定的时间间隔执行数据集的时间点快照。
  - AOF持久性记录服务器接收的每个写入操作，将在服务器启动时再次播放，重建原始数据集。使用与Redis协议本身相同的格式以仅追加方式记录命令。当Redis太大时，Redis能够重写日志背景。

- RDB的优缺点

  * 优点：
    * RDB最大限度地提高了Redis的性能，父进程不需要参与磁盘I/O,而是fork出一个子进程来备份
    * 与AOF相比，RDB允许使用大数据集更快地重启
  * 缺点：
    * 如果您需要在Redis停止工作时（例如断电后）将数据丢失的可能性降至最低，则RDB并不好,因为它的备份时间长,备份数量大
    * RDB经常需要fork（）才能使用子进程持久存储在磁盘上。如果数据集很大，Fork（）可能会非常耗时

- AOF的优缺点

  * 优点：

    * 性能消耗更小
    * 可以修改追加命令,比如flushall,只需要停机把aof末尾的命令删除
    * AOF的日志易于理解

  * 缺点：

    * AOF文件通常比同一数据集的等效RDB文件大
    * 根据确切的fsync策略，AOF可能比RDB慢

- RDB 和 AOF ,我应该用哪一个？
  一般来说,如果想达到足以媲美 PostgreSQL 的数据安全性， 你应该同时使用两种持久化功能。如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失， 那么你可以只使用 RDB 持久化。有很多用户都只使用 AOF 持久化， 但我们并不推荐这种方式： 因为定时生成 RDB 快照（snapshot）非常便于进行数据库备份， 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快

* 在线上我们到底该怎么做？

  * RDB持久化与AOF持久化同步使用

  * 自己制定策略定期检查Redis的情况，然后可以手动触发备份、重写数据；

  * 采用集群和主从同步

# 8. 事务

一个事务包含了多个命令，服务器在执行事务期间，不会改去执行其它客户端的命令请求。

事务中的多个命令被一次性发送给服务器，而不是一条一条发送，这种方式被称为流水线，它可以减少客户端与服务器之间的网络通信次数从而提升性能。

Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。

## 8.1. redis事务相关命令
1. MULTI 与 EXEC命令
以 MULTI 开始一个事务，然后将多个命令入队到事务中， 最后由 EXEC 命令触发事务， 一并执行事务中的所有命令
2. DISCARD命令
DISCARD 命令用于取消一个事务， 它清空客户端的整个事务队列， 然后将客户端从事务状态调整回非事务状态， 最后返回字符串 OK 给客户端， 说明事务已被取消。
3. WATCH命令
WATCH 命令用于在事务开始之前监视任意数量的键： 当调用 EXEC 命令执行事务时， 如果任意一个被监视的键已经被其他客户端修改了， 那么整个事务不再执行， 直接返回失败。

## 8.2. redis数据库事务的ACID与传统关系型事务的比较

ACID，是指数据库管理系统（DBMS）在写入或更新资料的过程中，为保证事务（transaction）是正确可靠的，所必须具备的四个特性：原子性（atomicity，或称不可分割性）、一致性（consistency）、隔离性（isolation，又称独立性）、持久性（durability）。

### 8.2.1. 原子性（Atomicity）
一个事务（transaction）中的所有操作，或者全部完成，或者全部不完成，不会结束在中间某个环节。事务在执行过程中发生错误，会被回滚（Rollback）到事务开始前的状态，就像这个事务从来没有执行过一样。即，事务不可分割、不可约简。

单个 Redis 命令的执行是原子性的，但 Redis 没有在事务上增加任何维持原子性的机制，所以 Redis 事务的执行并不是原子性的。

如果一个事务队列中的所有命令都被成功地执行，那么称这个事务执行成功。

另一方面，如果 Redis 服务器进程在执行事务的过程中被停止,或者命令执行错误，那么剩余命令会继续执行,同时不提供回滚

通过内置的 Lua 解释器，可以使用 EVAL 命令对 Lua 脚本进行求值。Redis 使用单个 Lua 解释器去运行所有脚本，并且， **Redis 也保证脚本会以原子性(atomic)的方式执行：当某个脚本正在运行的时候，不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似**。在其他别的客户端看来，脚本的效果(effect)要么是不可见的(not visible)，要么就是已完成的(already completed)。

### 8.2.2. 一致性（Consistency）
定义: 在事务开始之前和事务结束以后，数据库的完整性没有被破坏。

Redis 的一致性问题可以分为三部分来讨论：入队错误、执行错误、Redis 进程被终结。

**入队错误**
在命令入队的过程中，如果客户端向服务器发送了错误的命令，比如命令的参数数量不对，等等， 那么服务器将向客户端返回一个出错信息， 并且将客户端的事务状态设为 REDIS_DIRTY_EXEC 。

当客户端执行 EXEC 命令时， Redis 会拒绝执行状态为 REDIS_DIRTY_EXEC 的事务， 并返回失败信息。
```linux
redis 127.0.0.1:6379> MULTI
OK

redis 127.0.0.1:6379> set key
(error) ERR wrong number of arguments for 'set' command

redis 127.0.0.1:6379> EXISTS key
QUEUED

redis 127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
```
因此，带有不正确入队命令的事务不会被执行，也不会影响数据库的一致性。

**执行错误**

执行错误的命令,但对其他事务没有影响

如果命令在事务执行的过程中发生错误，比如说，对一个不同类型的 key 执行了错误的操作， 那么 Redis 只会将错误包含在事务的结果中， 这不会引起事务中断或整个失败，不会影响已执行事务命令的结果，也不会影响后面要执行的事务命令， 所以它对事务的一致性也没有影响。

**Redis 进程被终结**
如果 Redis 服务器进程在执行事务的过程中被其他进程终结，或者被管理员强制杀死，那么根据 Redis 所使用的持久化模式，可能有以下情况出现：

内存模式：如果 Redis 没有采取任何持久化机制，那么重启之后的数据库总是空白的，所以数据总是一致的。

+ RDB 模式：在执行事务时，Redis 不会中断事务去执行保存 RDB 的工作，只有在事务执行之后，保存 RDB 的工作才有可能开始。所以当 RDB 模式下的 Redis 服务器进程在事务中途被杀死时，事务内执行的命令，不管成功了多少，都不会被保存到 RDB 文件里。恢复数据库需要使用现有的 RDB 文件，而这个 RDB 文件的数据保存的是最近一次的数据库快照（snapshot），所以它的数据可能不是最新的，但只要 RDB 文件本身没有因为其他问题而出错，那么还原后的数据库就是一致的。

+ AOF 模式：因为保存 AOF 文件的工作在后台线程进行，所以即使是在事务执行的中途，保存 AOF 文件的工作也可以继续进行，因此，根据事务语句是否被写入并保存到 AOF 文件，有以下两种情况发生：

    1）如果事务语句未写入到 AOF 文件，或 AOF 未被 SYNC 调用保存到磁盘，那么当进程被杀死之后，Redis 可以根据最近一次成功保存到磁盘的 AOF 文件来还原数据库，只要 AOF 文件本身没有因为其他问题而出错，那么还原后的数据库总是一致的，但其中的数据不一定是最新的。

    2）如果事务的部分语句被写入到 AOF 文件，并且 AOF 文件被成功保存，那么不完整的事务执行信息就会遗留在 AOF 文件里，当重启 Redis 时，程序会检测到 AOF 文件并不完整，Redis 会退出，并报告错误。需要使用 redis-check-aof 工具将部分成功的事务命令移除之后，才能再次启动服务器。还原之后的数据总是一致的，而且数据也是最新的（直到事务执行之前为止）。

### 8.2.3. 隔离性（Isolation）
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力，隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别，包括未提交读（Read uncommitted）、提交读（read committed）、可重复读（repeatable read）和串行化（Serializable）。

Redis 是单进程程序，并且它保证在执行事务时，不会对事务进行中断，事务可以运行直到执行完所有事务队列中的命令为止。因此，Redis 的事务是总是带有隔离性的。

### 8.2.4. 持久性（Durability）
因为事务不过是用队列包裹起了一组 Redis 命令，并没有提供任何额外的持久性功能，所以事务的持久性由 Redis 所使用的持久化模式决定：

在单纯的内存模式下，事务肯定是不持久的。

在 RDB 模式下，服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败，所以 RDB 模式下的 Redis 事务也是不持久的。

在 AOF 的“总是 SYNC ”模式下，事务的每条命令在执行成功之后，都会立即调用 fsync 或 fdatasync 将事务数据写入到 AOF 文件。但是，这种保存是由后台线程进行的，主线程不会阻塞直到保存成功，所以从命令执行成功到数据保存到硬盘之间，还是有一段非常小的间隔，所以这种模式下的事务也是不持久的。

其他 AOF 模式也和“总是 SYNC ”模式类似，所以它们都是不持久的。

## 10.3. redis主从集群的搭建与docker通信知识
### 10.3.1. docker搭建redis主从集群
[docker搭建redis主从集群](https://blog.csdn.net/qq_28804275/article/details/80907796)
[容器之间软连接通信](https://www.9ge6.com/archives/392.html)
[容器之间的四种通信方式](https://www.cnkirito.moe/docker-network-bridge/)
[link的底层原理](https://www.jianshu.com/p/21d66ca6115e)
查看docker所有容器的ip: `docker inspect -f '{{.Name}} - {{.NetworkSettings.IPAddress }}' $(docker ps -aq)`
改进点:

+ redis.conf的protected mode 关闭,不然会导致spring boot 访问失败
+ 同一主机的docker容器间的访问(redis里面设置127.0.0.1 会识别成自己容器内的网址,而不是宿主机的网址),创建slave容器的时候应该是`docker run --link redis-master:redis-master  -it --name redis-slave -v /root/:/usr/local/etc/redis -d -p 6301:6379 redis /bin/bash`,使用--link就可以使主从容器之间
+  redis-slave.conf 里面的slaveof masterport使用master容器内部port 6379即可,不要用绑定到外部的port

用link实现之后,又学到了自定义桥接模式
[自定义桥接模式参考博客](https://www.cnkirito.moe/docker-network-bridge/)
[桥接模式下的三种访问容器方式](https://birdben.github.io/2017/05/02/Docker/Docker%E5%AE%9E%E6%88%98%EF%BC%88%E4%BA%8C%E5%8D%81%E4%B8%83%EF%BC%89Docker%E5%AE%B9%E5%99%A8%E4%B9%8B%E9%97%B4%E7%9A%84%E9%80%9A%E4%BF%A1/)

自定义桥接模式内部互相暴露端口,可以通过别名访问,也可以对宿主机暴露端口;
但自定义网络内部不能访问宿主机下的端口服务

link不如自定义network
link: hosts
自定义network: 在容器内部创建了dns

### 10.3.2. docker 搭建redis-sentinel集群
[docker搭建redis-sentinel集群](https://cloud.tencent.com/developer/article/1343834)
改进点:
+ 运行sentinel的时候把26000端口暴露出来,以供外界访问
docker run -it --name redis-sentienl0container `-p 26000:26000 ` -v /root/:/usr/local/etc/redis/ -d  redis:4.0 /bin/bash

+ 自定义sentinel.conf的时候, master ip不要写localhost或者127.0.0.1 ; 而是要写服务器所在局域网的ip,比如 192.168.245.4;这样spring boot 通过sentinel连接redis节点的时候,就不会出现连接127.0.0.1:6300而找不到redis节点.

# 11. 订阅机制
1. PUBLISH将信息message发送到指定的频道channe1。返回收到消息的客户端数量
2. SUBSCRIBE订阅给指定频道的信息
3. UNSUBSCRTBE取消订阅指定的频道，如果不指定，则取消订阅所有的频道。

## 11.1. redis的消息订阅发布和mq对比？

答：redis发布订阅功能比较薄弱但比较轻量级，mq消息持久化，数据可靠性比较差，无后台功能可msgId、msgKey进行查询消息

# 12. redis作为二级缓存

## 12.1. 实现
导入ibatis,导入redis,在访问数据的时候,先访问redis是否存在,存在就返回,不存在就去mysql寻找,并放入缓存.

**常用注解:**

@CacheConfig(cacheNames="userInfoCache")  在同个redis里面必须唯一

@Cacheable(查) ：
            来划分可缓存的方法 - 即，结果存储在缓存中的方法，以便在后续调用（具有相同的参数）时，返回缓存中的值而不必实际执行该方法

@CachePut（修改、增加） ：
            当需要更新缓存而不干扰方法执行时，可以使用@CachePut注释。也就是说，始终执行该方法并将其结果放入缓存中（根据@CachePut选项）

@CacheEvict（删除） ：
            对于从缓存中删除陈旧或未使用的数据非常有用，指示缓存范围内的驱逐是否需要执行而不仅仅是一个条目驱逐# 缓存雪崩

## 12.2. springboot cache自定义项

1)自定义KeyGenerator

2)自定义cacheManager，设置缓存过期时间

3）自定义序列化方式，Jackson

## 12.3. mysql与redis性能对比

redis缓存和数据库的对比

```
1、apache abtest
          ab是Apache HTTP server benchmarking tool，可以用以测试HTTP请求的服务器性能

2、abtest的安装
          1）yum install -y httpd-tools
          2）ab -V检验是否安装成功
3、ab -n1000 -c10 http://localhost:8080/getByCache?id=2
   ab -n1000 -c10 http://localhost:8080/getUser?id=2

         1）-n:进行http请求的总个数
         2）-c:请求的client个数，也就是请求并发数
         3)统计qps：qps即每秒并发数，request per second
    统计：10个并发的情况下
        redis qps：963.85[#/sec] (mean)
        DB qps： 766.75 [#/sec] (mean)

        100个并发的情况下 1000个
        redis qps：1130.60 [#/sec] (mean)
        DB qps：956.15 [#/sec] (mean)

        100个并发的情况下，进行10000个请求
        redsi qps: 2102.39 [#/sec] (mean)
        DB qps: 679.07 [#/sec] (mean)
        500个并发的情况下，进行10000个请求
        redis qps：374.91 [#/sec] (mean)
        DB qps：扛不住
```

# 13. 缓存穿透与缓存雪崩
## 13.1. 缓存穿透
什么是缓存穿透？

缓存穿透是指用户查询数据，在数据库没有，自然在缓存中也不会有。这样就导致用户查询的时候，在缓存中找不到对应key的value，每次都要去数据库再查询一遍，然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库

你有什么解决方案来防止缓存穿透？

1、缓存空值
如果一个查询返回的数据为空(不管是数据不 存在，还是系统故障)我们仍然把这个空结果进行缓存，但它的过期时间会很短，最长不超过五分钟。 通过这个直接设置的默认值存放到缓存，这样第二次到缓冲中获取就有值了，而不会继续访问数据库

2、采用布隆过滤器BloomFilter  优势占用内存空间很小，bit存储。性能特别高。
将所有可能存在的数据哈希到一个足够大的 bitmap 中，一个一定不存在的数据会被这个bitmap 拦截掉，从而避免了对底层存储系统的查询压力

## 13.2. 缓存雪崩

如果缓存集中在一段时间内失效，发生大量的缓存穿透，所有的查询都落在数据库上，造成了缓存雪崩。

由于原有缓存失效，新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了，而对数据库CPU 和内存造成巨大压力，严重的会造成数据库宕机

## 13.3. 缓存雪崩解决方案

1、加锁排队
key： whiltList
value：1000w个uid
指定setNxwhiltList value nullValue
mutex互斥锁解决，Redis的SETNX去set一个mutex key，
当操作返回成功时，再进行load db的操作并回设缓存；
否则，就重试整个get缓存的方法

2、数据预热
缓存预热就是系统上线后，将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候，先查询数据库，然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
可以通过缓存reload机制，预先去更新缓存，再即将发生大并发访问前手动触发加载缓存不同的key

3、双层缓存策略
C1为原始缓存，C2为拷贝缓存，C1失效时，可以访问C2，C1缓存失效时间设置为短期，C2设置为长期。

4、定时更新缓存策略
失效性要求不高的缓存，容器启动初始化加载，采用定时任务更新或移除缓存

5、设置不同的过期时间，让缓存失效的时间点尽量均匀

# 14. redis实现分布式锁(解决不同客户端并发操作某个数据)

## 14.1. 分布式锁是什么

* * 分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现

  * 如果不同的系统或同一个系统的不同主机之间共享了某个资源时，往往通过互斥来防止彼此干扰。

* 分布锁设计目的

 可以保证在分布式部署的应用集群中，同一个方法在同一操作只能被一台机器上的一个线程执行。

* 设计要求
  * 这把锁要是一把可重入锁（避免死锁）
  * 这把锁有高可用的获取锁和释放锁功能
  * 这把锁获取锁和释放锁的性能要好…

## 14.2. 分布锁实现方案分析
  * 获取锁的时候，使用 setnx(SETNX key val:当且仅当 key 不存在时，set 一个 key 为 val 的字符串，返回 1;
  * 若 key 存在，则什么都不做，返回 【0】加锁，锁的 value 值为当前占有锁服务器内网IP编号拼接任务标识
  * 在释放锁的时候进行判断。并使用 expire 命令为锁添 加一个超时时间，超过该时间则自动释放锁。
  * 返回1则成功获取锁。还设置一个获取的超时时间， 若超过这个时间则放弃获取锁。setex（key,value,expire）过期以秒为单位
  * 释放锁的时候，判断是不是该锁（即Value为当前服务器内网IP编号拼接任务标识），若是该锁，则执行 delete 进行锁释放

### 14.2.1. 分布式锁面试题

**简介：分析支付宝2018分布式锁面试题**

- 问题1：
      1、什么是分布式锁？

  * 首先，为了确保分布式锁可用，我们至少要确保锁的实现同时满足以下三个条件：
    * 互斥性。在任意时刻，只有一个客户端能持有锁。
    * 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁，也能保证后续其他客户端能加锁。
    * 解铃还须系铃人。加锁和解锁必须是同一个客户端，客户端自己不能把别人加的锁给解了

- * 从 Redis 2.6.0 版本开始，通过内置的 Lua 解释器，可以使用 EVAL 命令对 Lua 脚本进行求值。

  - Redis 使用单个 Lua 解释器去运行所有脚本，并且， Redis 也保证脚本会以原子性(atomic)的方式执行：当某个脚本正在运行的时候，不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来，脚本的效果(effect)要么是不可见的(not visible)，要么就是已完成的(already completed)。

- 问题2：怎么实现分布式锁

  - 实现分布式锁的方案大概有两种
    * 采用lua脚本操作分布式锁
    * 采用setnx、setex命令连用的方式实现分布式锁

- 问题3： 解锁需要注意什么

      解铃还须系铃人。加锁和解锁必须是同一个客户端，客户端自己不能把别人加的锁给解了

# 15. redis高可用集群

## 15.1. 主从复制的原理

- 进行复制中的主从服务器双方的数据库将保存相同的数据，概念上将这种现象称作“数据库状态一致”

- redis2.8版本之前使用旧版复制功能SYNC

  * SYNC是一个非常耗费资源的操作
    * 主服务器需要执行BGSAVE命令来生成RDB文件(一种数据持久化方式,全量持久化,与aof对应)，这个生成操作会耗费主服务器大量的的CPU、内存和磁盘读写资源
    * 主服务器将RDB文件发送给从服务器，这个发送操作会耗费主从服务器大量的网络带宽和流量，并对主服务器响应命令
    * 请求的时间产生影响：接收到RDB文件的从服务器在载入文件的过程是阻塞的，无法处理命令请求

- 2.8之后使用PSYNC

  - PSYNC命令具有完整重同步（full resynchronization）和部分重同步（partial resynchronization1）两种模式

    *  部分重同步功能由以下三个部分构成：
      * 主服务的复制偏移量（replication offset）和从服务器的复制偏移量
      * 主服务器的复制积压缓冲区（replication backlog）,默认大小为1M,里面存放着命令
      * 服务器的运行ID(run ID),用于存储服务器标识，如从服务器断线重新连接，取到主服务器的运行ID与重接后的主服务器运行ID进行对比，从而判断是执行部分重同步还是执行完整重同步

## 15.2. 分布式系统高可用

- 高可用的概念？

  - 高可用HA（High Availability）是分布式系统架构设计中必须考虑的因素之一，它通常是指，通过设计减少系统不能提供服务的时间。

- 通过三大要点解释高可用:

  - 单点是系统高可用的大敌，应该尽量在系统设计的过程中避免单点

  - 保证系统高可用，架构设计的核心准则是：冗余。

  - 每次出现故障需要人工介入恢复势必会增加系统的不可服务实践,实现自动故障转移

  - 重启服务，看服务是否每次都获取锁失败

- 分布式高可用经典架构环节分析

  - 【客户端层】到【反向代理层】的高可用，是通过反向代理层的冗余来实现的。以nginx为例：有两台nginx，一台对线上提供服务，另一台冗余以保证高可用， 常见的实践是keepalived存活探测
  - 【反向代理层】到【web应用】的高可用，是通过站点层的冗余来实现的。假设反向代理层是nginx，nginx.conf里能够配置多个web后端，并且nginx能够探测到多个后端的存活性。
    * 自动故障转移：当web-server挂了的时候，nginx能够探测到，会自动的进行故障转移，将流量自动迁移到其他的web-server，整个过程由nginx自动完成，对调用方是透明的。
  * 【服务层】到【缓存层】的高可用，是通过缓存数据的冗余来实现的。redis天然支持主从同步，redis官方也有sentinel哨兵机制，来做redis的存活性检测。
  * 【服务层】到【数据库层】的高可用，数据库层用“主从同步，读写分离”架构，所以数据库层的高可用，又分为“读库高可用”与“写库高可用”两类。
    * 读库采用冗余的方式实现高可用，写库采用keepalived存活探测   binlog进行同步

## 数据迁移的三种方式
1. rdb数据备份恢复方法
redis 127.0.0.1:6379> SAVE
OK
或者
redis-cli -h 127.0.0.1 -p 6379 -a pwd bgsave
该命令将在 redis 安装目录中创建dump.rdb文件。

查找dump.rdb文件位置
redis 127.0.0.1:6379> CONFIG GET dir
1) “dir”
2) “/usr/local/redis/bin”
以上命令 CONFIG GET dir 输出的 redis 安装目录为 /usr/local/redis/bin。
bgsave
创建 redis 备份文件也可以使用命令 BGSAVE，该命令在后台执行。
127.0.0.1:6379> BGSAVE
Background saving started

2. AOF数据备份恢复方法
另一种持久化方式AOF，在配置文件中打开[appendonly yes]。
AOF刷新日志到disk的规则：
appendfsync always #always 表示每次有写操作都进行同步，非常慢，非常安全。
appendfsync everysec #everysec表示对写操作进行累积，每秒同步一次
官方的建议的everysec，安全，就是速度不够快，如果是机器出现问题可能会丢失1秒的数据。

也可以手动执行bgrewriteaof进行AOF备份：
redis-cli -h 127.0.0.1 -p 6379 -a pwd bgrewriteaof

迁移数据恢复
迁移到另外一台恢复数据，需先检查配置文件，将按照以下优先级恢复数据到内存：
如果只配置AOF,重启时加载AOF文件恢复数据；
如果同时 配置了RBD和AOF,启动是只加载AOF文件恢复数据；
如果只配置RBD,启动是讲加载dump文件恢复数据；

dump.rdb或者AOF文件迁移到另外一台恢复数据
恢复数据，只需将备份文件 (dump.rdb或者AOF文件) 移动到 redis 安装目录并启动服务即可。

3. 挂redis从库复制数据方法
Redis提供了复制（replication）功能可以自动实现同步的过程。
配置方法
通过配置文件 从数据库的配置文件中加入slaveof master-ip master-port，主数据库无需配置
通过命令行参数 启动redis-server的时候，使用命令行参数–slaveof master-ip master port
redis-server –port 6380 –slaveof 127.0.0.1 6379
通过命令SLAVEOF master-ip master-port
redis>SLAVEOF 127.0.0.1 6379
SLAVEOF NO ONE可以是当前数据库停止接收其他数据库的同步，转成主Redis数据库，程序连接地址都改为新的redis库IP地址和端口。

## 15.3. Redis高可用架构Sentinel(解决宕机)
**sentinel(中文就是哨兵的意思)是为了主从之间的自动切换(自动高可用),cluster用于分散hash负载均衡**

**简介：互联网服务灾备故障转移，sentinel的配置**

* `Redis`主从复制可将主节点数据同步给从节点，从节点此时有两个作用：
  - 一旦主节点宕机，从节点作为主节点的备份可以随时顶上来。
  - 扩展主节点的读能力，分担主节点读压力。

  但是问题来了：

  - 一旦主节点宕机，从节点晋升成主节点，同时需要修改应用方的主节点地址，还需要命令所有从节点去复制新的主节点，整个过程需要人工干预。

- redis主节点挂掉之后应该怎么操作？命令模拟

  ```shell
   slaveof no one       # 取消主备，变更为主节点

   slaveof 新host  新节点  # 将其他节点设置为新主节点的备份节点
  ```

- Sentinel正是实现了这个功能

- 开启Sentinel配置       3主3从   3主6从

  ```powershell
  sentinel monitor mymaster 127.0.0.1 6379 1
  sentinel down-after-milliseconds mymaster 10000
  sentinel failover-timeout mymaster 60000
  sentinel parallel-syncs mymaster 1

  ```
- 命令讲解

  * sentinel monitor mymaster 127.0.0.1 6379 1 名称为mymaster的主节点名，1表示将这个主服务器判断为失效至少需要 1个 Sentinel 同意 （只要同意 Sentinel 的数量不达标，自动故障迁移就不会执行）
  * down-after-milliseconds 选项指定了 Sentinel 认为服务器已经断线所需的毫秒数
  *  failover过期时间，当failover开始后，在此时间内仍然没有触发任何failover操作，当前sentinel  将会认为此次failoer失败
  * parallel-syncs 选项指定了在执行故障转移时， 最多可以有多少个从服务器同时对新的主服务器进行同步， 这个数字越小， 完成故障转移所需的时间就越长。

  * 如果从服务器被设置为允许使用过期数据集， 那么你可能不希望所有从服务器都在同一时间向新的主服务器发送同步请求， 因为尽管复制过程的绝大部分步骤都不会阻塞从服务器， 但从服务器在载入主服务器发来的 RDB 文件时， 仍然会造成从服务器在一段时间内不能处理命令请求： 如果全部从服务器一起对新的主服务器进行同步， 那么就可能会造成所有从服务器在短时间内全部不可用的情况出现。

- 启动所有主从上的sentinel

  *  前提是它们各自的server已成功启动
        cd /usr/local/redis/src/redis-sentinel /etc/redis/sentinel.conf

- info Replication  查看节点信息

- shutdown主节点看服务是否正常

### 15.3.1. Sentinel三大工作任务

  - 监控（Monitoring）： Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  - 提醒（Notification）： 当被监控的某个 Redis 服务器出现问题时， Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  - 自动故障迁移（Automatic failover）： 当一个主服务器不能正常工作时， Sentinel 会开始一次自动故障迁移操作， 它会将失效主服务器的其中一个从服务器升级为新的主服务器， 并让失效主服务器的其他从服务器改为复制新的主服务器； 当客户端试图连接失效的主服务器时， 集群也会向客户端返回新主服务器的地址， 使得集群可以使用新主服务器代替失效服务器。
  - 可以当成一个反向代理服务器来用

### 15.3.2. 冷备和热备

  * 冷备

    *  概念：冷备份发生在数据库已经正常关闭的情况下，当正常关闭时会提供给我们一个完整的数据库
    * 优点：
      * 是非常快速的备份方法（只需拷文件）
      * 低度维护，高度安全
    * 缺点：
      - 单独使用时，只能提供到“某一时间点上”的恢复
      - 再实施备份的全过程中，数据库必须要作备份而不能作其他工作。也就是说，在冷备份过程中，数据库必须是关闭状态

  * 热备

    *   概念：热备份是在数据库运行的情况下，采用archivelog mode方式备份数据库的方法

    *   优点：
      * 备份的时间短
      * 备份时数据库仍可使用
      * 可达到秒级恢复
    *   缺点
      * 若热备份不成功，所得结果不可用于时间点的恢复
      * 因难于维护，所以要非凡仔细小心

### 15.3.3. Sentinel故障转移原理

**简介：Sentinel是怎么工作的？**

- 主观下线：

  - 概念主观下线（Subjectively Down， 简称 SDOWN）指的是单个 Sentinel 实例对服务器做出的下线判断

  - 主观下线特点：

    * 如果一个服务器没有在 master-down-after-milliseconds 选项所指定的时间内， 对向它发送 PING 命令的 Sentinel 返回一个有效回复（valid reply）， 那么 Sentinel 就会将这个服务器标记为主观下线

    * 服务器对 PING 命令的有效回复可以是以下三种回复的其中一种：
         ```shell
         返回 +PONG 。
         返回 -LOADING 错误。
         返回 -MASTERDOWN 错误。
         ```

- 客观下线

  - 客观下线概念：

    * 指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断， 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后， 得出的服务器下线判断。 （一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。）

  - 客观下线特点：

    * 从主观下线状态切换到客观下线状态并没有使用严格的法定人数算法（strong quorum algorithm）， 而是使用了流言协议： 如果 Sentinel 在给定的时间范围内， 从其他 Sentinel 那里接收到了足够数量的主服务器下线报告， 那么 Sentinel 就会将主服务器的状态从主观下线改变为客观下线。 如果之后其他 Sentinel 不再报告主服务器已下线， 那么客观下线状态就会被移除。

  - 客观下线注意点：
    * 客观下线条件只适用于主服务器： 对于任何其他类型的 Redis 实例， Sentinel 在将它们判断为下线前不需要进行协商， 所以从服务器或者其他 Sentinel 永远不会达到客观下线条件。 只要一个 Sentinel 发现某个主服务器进入了客观下线状态， 这个sentinel 就可能会被其他 Sentinel 推选出， 并对失效的主服务器执行自动故障迁移操作。

### 15.3.4. sentinel整合Springboot实战

**简介：Redis高可用整合springboot讲解**

- 思考？redis高可用sentinel整合springboot的步骤

   图解高可用架构代理的概念

- 引入yml配置文件

  ```
  redis:
        sentinel:
           master: redis_master_group1    #mymaster
           nodes: 172.16.244.133:26379
  ```

- pom文件

  ```java
  <dependency>
  			<groupId>org.springframework.boot</groupId>
  			<artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  ```
- 启动springboot服务

- 登上服务器看配置是否有效

## 15.4. Redis内置高可用集群

### 15.4.1. Redis集群搭建

[我的github秒杀项目repository](https://github.com/sx89/seckill-practice)

### 15.4.2. 一致性Hash算法

[参考博客](https://juejin.im/post/5ae1476ef265da0b8d419ef2#heading-2)

  2的32次方进行hash取模     0到2的32次方-1

- jedis分布式之 ShardedJedisPool （一致性Hash分片算法）

- 概念：

  分布式系统中负载均衡的问题时候可以使用Hash算法让固定的一部分请求落到同一台服务器上，这样每台服务器固定处理一部分请求（并维护这些请求的信息），起到负载均衡的作用

- 做法：

  * hash环上顺时针从整数0开始，一直到最大正整数，我们根据四个ip计算的hash值肯定会落到这个hash环上的某一个点，至此我们把服务器的四个ip映射到了一致性hash环
  * 当用户在客户端进行请求时候，首先根据hash(用户id)计算路由规则（hash值），然后看hash值落到了hash环的那个地方，根据hash值在hash环上的位置顺时针找距离最近的ip作为路由ip

#### 一致性hash的特性
  - hash的特性
       * 单调性(Monotonicity)，单调性是指如果已经有一些请求通过哈希分派到了相应的服务器进行处理，又有新的服务器加入到系统中时候，应保证原有的请求可以被映射到原有的或者新的服务器中去，而不会被映射到原来的其它服务器上去。

       * 分散性(Spread)：(同一个请求落到了多个服务器上,增大网络消耗,一致性hash具有很低的分散性) 分布式环境中，客户端请求时候可能不知道所有服务器的存在，可能只知道其中一部分服务器，在客户端看来他看到的部分服务器会形成一个完整的hash环。如果多个客户端都把部分服务器作为一个完整hash环，那么可能会导致，同一个用户的请求被路由到不同的服务器进行处理。这种情况显然是应该避免的，因为它不能保证同一个用户的请求落到同一个服务器。所谓分散性是指上述情况发生的严重程度。好的哈希算法应尽量避免尽量降低分散性。 一致性hash具有很低的分散性

       * 平衡性(Balance)：平衡性也就是说负载均衡，是指客户端hash后的请求应该能够分散到不同的服务器上去。一致性hash可以做到每个服务器都进行处理请求，但是不能保证每个服务器处理的请求的数量大致相同

- 新增节点

  * 在系统中增加一台服务器Node X,此时对象Object A、B、D不受影响，只有对象C需要重定位到新的Node X 。一般的，在一致性哈希算法中，如果增加一台服务器，则受影响的数据仅仅是新服务器到其环空间中前一台服务器（即沿着逆时针方向行走遇到的第一台服务器）之间数据，其它数据也不会受到影响。

### 15.4.3. 一致性Hash算法虚拟节点

**简介：通过虚拟节点倾斜解决方案，均匀一致性hash**

- 出现问题分析：

      部门hash节点下架之后，虽然剩余机器都在处理请求，但是明显每个机器的负载不均衡，这样称为一致性hash的倾斜，虚拟节点的出现就是为了解决这个问题。

- 增设虚拟节点

     当物理机器数目为A，虚拟节点为B的时候，实际hash环上节点个数为A*B，将A节点分部为A1,A2,A3;将A1、A2、A3平均分布在各个位置，使A服务的节点尽量均匀分配在各个角落

     当物理机器数目为3，虚拟节点为ABCD的时候，实际hash环上节点个数为12个，将A节点分部为A1,A2,A3;将A1、A2、A3平均分布在各个位置，这样第一台物理机拥有的虚拟节点是 A1 B1 C1 D1 ;这样第二台物理机拥有的虚拟节点是 A2 B2 C2 D2 ;这样第3台物理机拥有的虚拟节点是 A3 B3 C3 D3.这样某一台物理机挂了,其他两台机器可以负载均衡.

     当某个节点挂了之后，其数据均衡的分布给相邻的顺时针后面的一个节点上面，故所有数据比之前所述一致性hash相对均衡

### 15.4.4. twemproxy实现hash分片

**简介：经典服务端分片twemproxy特性讲解**

codis

- 概念：

      Twemproxy，也叫nutcraker。是一个twtter开源的一个redis和memcache快速/轻量级代理服务器; Twemproxy是一个快速的单线程代理程序，支持Memcached 和Redis。Redis代理中间件twemproxy是一种利用中间件做分片的技术。twemproxy处于客户端和服务器的中间，将客户端发来的请求，进行一定的处理后（sharding），再转发给后端真正的redis服务器

- 作用：

     Twemproxy通过引入一个代理层，可以将其后端的多台Redis或Memcached实例进行统一管理与分配，使应用程序只需要在Twemproxy上进行操作，而不用关心后面具体有多少个真实的Redis或Memcached存储

- 特性：
  * 支持失败节点自动删除
    * 可以设置重新连接该节点的时间
    * 可以设置连接多少次之后删除该节点
  * 减少了客户端直接与服务器连接的连接数量
    * 自动分片到后端多个redis实例上
  * 多种哈希算法
    * md5，crc16，crc32，crc32a，fnv1_64，fnv1a_64，fnv1_32，fnv1a_32，hsieh，murmur，jenkins
  * 多种分片算法
    * ketama（一致性hash算法的一种实现），modula，random

# 16. 布隆过滤器

### 16.0.5. 介绍

- 概念：

  - **布隆过滤器**（英语：Bloom Filter）是1970年由布隆提出的。它实际上是一个很长的[二进制](https://zh.wikipedia.org/wiki/%E4%BA%8C%E8%BF%9B%E5%88%B6)向量和一系列随机[映射函数](https://zh.wikipedia.org/wiki/%E6%98%A0%E5%B0%84)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法，缺点是有一定的误识别率和删除困难。
- 优点:

  - 相比于其它的数据结构，布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数（{\displaystyle O(k)}![O(k)](https://wikimedia.org/api/rest_v1/media/math/render/svg/f5ec39041121b14e8c2b1a986c9b04547b223e3c)）。另外，散列函数相互之间没有关系，方便由硬件并行实现。布隆过滤器不需要存储元素本身，在某些对保密要求非常严格的场合有优势
- 缺点
  - 但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加，误算率随之增加。但是如果元素数量太少，则使用散列表足矣

### 16.0.6. 使用场景

**网页爬虫对URL的去重，避免爬取相同的URL地址；**

  **反垃圾邮件，从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱（同理，垃圾短信）；**

  **缓存击穿，将已存在的缓存放到布隆中，当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。**

 **抢红包项目中,抢红包分为抢和拆,抢的一步可以用布隆过滤器,拆的一步,把抢的那些用户请求过滤掉**

## 16.1. 过滤器原理

<div align="center"> <img src=" .\pictures\redis\Snipaste_2019-09-11_16-59-45.jpg" width="650px"> </div><br>

以上图为例，具体的操作流程：假设集合里面有3个元素{x, y, z}，哈希函数的个数为3。首先将位数组进行初始化，将里面每个位都设置位0。对于集合里面的每一个元素，将元素依次通过3个哈希函数进行映射，每次映射都会产生一个哈希值，这个值对应位数组上面的一个点，然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候，同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1，则可以判断该元素一定不存在集合中。反之，如果3个点都为1，则该元素可能存在集合中。注意：此处不能判断该元素是否一定存在集合中，可能存在一定的误判率。可以从图中可以看到：假设某个元素通过映射对应下标为4，5，6这3个点。虽然这3个点都为1，但是很明显这3个点是不同元素经过哈希得到的位置，因此这种情况说明元素虽然不在集合中，也可能对应的都是1，这是误判率存在的原因。

无非是一些好人也被抓，这个可以通过给这些可伶的被误伤的设置个白名单就OK。但是能屏蔽大量坏人的作用是很重要的.

## 16.2. goole布隆过滤器与Redis布隆过滤器

布隆过滤器两种实现方案的优缺点分析

- google布隆过滤器的缺陷与思考

  基于内存布隆过滤器有什么特点

  - 内存级别产物
  - 重启即失效
  - 本地内存无法用在分布式场景
  - 不支持大数据量存储

- 需求分析步骤

  - 互联网功能需求分析
    - 这是一个抽奖程序，只针对会员用户有效
  - 抽离出功能所有api
  - 制定存储方案
  - 性能优化方案分析

- Redis布隆过滤器

  * 可扩展性Bloom过滤器
    * 一旦Bloom过滤器达到容量，就会在其上创建一个新的过滤器
  * 不存在重启即失效或者定时任务维护的成本
    * 基于goole实现的布隆过滤器需要启动之后初始化布隆过滤器
  * 缺点：
    * 需要网络IO,性能比基于内存的过滤器低

- 选择:

     优先基于数据量进行考虑

## 布隆过滤器扩容

因为布隆过滤器的不可逆，我们没法重新建一个更大的布隆过滤器然后去把数据重新导入。这边采取的扩容的方法是，保留原有的布隆过滤器，建立一个更大的，新增数据都放在新的布隆过滤器中，去重的时候检查所有的布隆过滤器。

## 16.3. Redis布隆过滤器安装

- Redis布隆过滤器安装过程    自己构建一个bitMap
-
  * 您应该首先下载并编译模块：

    ```powershell
     $ git clone git：//github.com/RedisLabsModules/rebloom
     $ cd rebloom
     $ make
    ```

  * 将Rebloom加载到Redis中，在redis.conf里面添加

    ```powershell
    loadmodule /path/to/rebloom.so
    ```

  * 命令实战

    ```powershell
    BF.ADD bloom redis
    BF.EXISTS bloom redis
    BF.EXISTS bloom nonxist
    ```

## 16.4. Redis布隆过滤器与springboot的整合探索

**简介：基于lua脚本实现springboot和布隆过滤器的整合**

- 通过普通命令无法实现springboot整合布隆过滤器
- 查找github开源框架的流程
- 分析开源框架的实现原理
- 通过lua脚本自己实现布隆过滤器
- 编写两个lua脚本
  * 添加数据到指定名称的布隆过滤器
  * 从指定名称的布隆过滤器获取key是否存在的脚本

# redis的并发竞争问题(修改一个值,商品超卖)

Redis是一种单线程机制的nosql数据库，基于key-value，数据可持久化落盘。由于单线程所以Redis本身并没有锁的概念，多个客户端连接并不存在竞争关系，但是利用jedis等客户端对Redis进行并发访问时会出现问题。

## 解决方案
### 分布式锁

#### 数据库实现方式

#### zookeeper实现

#### redis实现

https://www.jianshu.com/p/3582556fe6f1

在类似秒杀这样的并发场景下，为了确保同一时刻只能允许一个用户访问资源，需要利用加锁的机制控制资源的访问权。如果服务只在单台机器上运行，可以简单地用一个内存变量进行控制。而在多台机器的系统上，则需要用分布式锁的机制进行并发控制。基于redis的一些特性，利用redis可以既方便又高效地模拟锁的实现。

**简单方案**

如果lock_key不存在，那么就设置lock_key的值为1，并且设置过期时间；如果lock_key存在，说明已经有人在使用这把锁，访问失败。

```python
def acquire_lock(lock_key, expire_timeout=60):
    if redis.setnx(lock_key, 1):
        redis.expire(lock_key, expire_timeout)
        return True
    return False
```
异常情况：如果setnx设置成功，但expire由于某些原因（比如超时）操作失败，那么这把锁就永远存在了，也就是所谓的死锁，后面的人永远无法访问这个资源。

**利用时间戳取值的方案**

为了解决死锁，我们可以利用setnx的value来做文章。上例中的我们设的value是1，其实并没有派上用场。因此可以考虑将value设为当前时间加上expire_timeout，当setnx设置失败后，我们去读lock_key的value，并且和当前时间作比对，如果当前时间大于value，那么资源理当被释放。代码示例如下：

```python
def acquire_lock(lock_key, expire_timeout=60):
    expire_time = int(time.time()) + expire_timeout
    if redis.setnx(lock_key, expire_time):
        redis.expire(lock_key, expire_timeout)
        return True
    redis_value = redis.get(lock_key)
    if redis_value and int(time.time()) > int(redis_value):
        redis.delete(lock_key)
    return False
```
然而仔细推敲下这段代码仍然能发现一些问题。第一，这个方案依赖时间，如果在分布式系统中的时间没有同步，则会对方案产生一定偏差。第二，假设C1和C2都没拿到锁，它们都去读value并对比时间，在竞态条件(race condition)下可能产生如下的时序：C1删除lock_key，C1获得锁，C2删除lock_key，C2获得锁。这样C1和C2同时拿到了锁，显然是不对的。

**GETSET改进方案**

GETSET指令在set新值的同时会返回老的值，这样的话我们可以检查返回的值，如果该值和之前读出来的值相同，那么这次操作有效，反之则无效。代码示例如下：

```python
def acquire_lock(lock_key, expire_timeout=60):
    expire_time = int(time.time()) + expire_timeout
    if redis.setnx(lock_key, expire_time):
        redis.expire(lock_key, expire_timeout)
        return True
    redis_value = redis.get(lock_key)
    if redis_value and int(time.time()) > int(redis_value):
        expire_time = int(time.time()) + expire_timeout
        old_value = redis.getset(lock_key, expire_time)
        if int(old_value) == int(redis_value):
            return True
    return False
```

这个方案基本可以满足要求，除了有一个小瑕疵，由于getset会去修改value，在竞态条件下可能会被修改多次导致timeout有细微的误差，但这个对结果影响不大。

**最终方法**

从redis 2.6.12版本开始有一个更为简便的方法。我们可以使用SET指令的扩展 ** SET key value [EX seconds] [PX milliseconds] [NX|XX] **，这个指令相当于对SETNX和EXPIRES进行了合并，因而我们的算法可以简化为如下一行：

```python
def acquire_lock(lock_key, expire_timeout=60):
    ret = redis.set(lock_key, int(time.time()), nx=True, ex=expire_timeout):
    return ret
```

### 利用消息队列
在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。

把Redis.set操作放在队列中使其串行化,必须的一个一个执行。

这种方式在一些高并发的场景中算是一种通用的解决方案。

### 用事务或者lua脚本实现乐观锁
使用乐观锁的方式进行解决（成本较低，非阻塞，性能较高）

本质上是假设不会进行冲突，使用redis的命令watch进行构造条件。伪代码如下：

复制代码
watch price

get price $price

$price = $price + 10

multi

set price $price

exec
复制代码
解释一下：

watch这里表示监控该key值，后面的事务是有条件的执行，如果从watch的exec语句执行时，watch的key对应的value值被修改了，则事务不会执行。

###  其他想法
分布式锁  + 分段优化  +  一致性hash (只是请求的转移,几个主节点存储一样的数据)把redis进行负载均衡

过滤大量请求,   用略大于商品数量的用户进来, 将秒杀分成两段,少量请求先抢redis商品数量才能下单mysql,redis原子加减分给商品数量,异步入库   有个问题是拿到号的不是真正的下单,会有少买问题,这个时候可以再把屏蔽的多余的号拿过来允许他们下单.  Mq异步入库

引入队列，然后将所有写DB操作在单队列中排队，完全串行处理。当达到库存阀值的时候就不在消费队列，并关闭购买功能。这就解决了超卖问题。

# redis与数据库的一致性(缓存入库万一mysql失败了怎么办)
## 读缓存时
读的时候，先读缓存，缓存没有的话，就读数据库，然后取出数据后放入缓存，同时返回响应。
## 写缓存时
更新的时候，先更新数据库，然后再删除缓存。

### 为什么是删除缓存，而不是更新缓存？

原因很简单，很多时候，在复杂点的缓存场景，缓存不单单是数据库中直接取出来的值。

比如可能更新了某个表的一个字段，然后其对应的缓存，是需要查询另外两个表的数据并进行运算，才能计算出缓存最新的值的。

另外更新缓存的代价有时候是很高的。是不是说，每次修改数据库的时候，都一定要将其对应的缓存更新一份？也许有的场景是这样，但是对于比较复杂的缓存数据计算的场景，就不是这样了。如果你频繁修改一个缓存涉及的多个表，缓存也频繁更新。但是问题在于，这个缓存到底会不会被频繁访问到？

举个栗子，一个缓存涉及的表的字段，在 1 分钟内就修改了 20 次，或者是 100 次，那么缓存更新 20 次、100 次；但是这个缓存在 1 分钟内只被读取了 1 次，有大量的冷数据。实际上，如果你只是删除缓存的话，那么在 1 分钟内，这个缓存不过就重新计算一次而已，开销大幅度降低。用到缓存才去算缓存。

其实删除缓存，而不是更新缓存，就是一个 lazy 计算的思想，不要每次都重新做复杂的计算，不管它会不会用到，而是让它到需要被使用的时候再重新计算。像 mybatis，hibernate，都有懒加载思想。查询一个部门，部门带了一个员工的 list，没有必要说每次查询部门，都把里面的 1000 个员工的数据也同时查出来啊。80% 的情况，查这个部门，就只是要访问这个部门的信息就可以了。先查部门，同时要访问里面的员工，那么这个时候只有在你要访问里面的员工的时候，才会去数据库里面查询 1000 个员工。

问题：先更新数据库，再删除缓存。如果删除缓存失败了，那么会导致数据库中是新数据，缓存中是旧数据，数据就出现了不一致。

解决思路：先删除缓存，再更新数据库。如果数据库更新失败了，那么数据库中是旧数据，缓存中是空的，那么数据不会不一致。因为读的时候缓存没有，所以去读了数据库中的旧数据，然后更新到缓存中。

比较复杂的数据不一致问题分析
数据发生了变更，先删除了缓存，然后要去修改数据库，此时还没修改。一个请求过来，去读缓存，发现缓存空了，去查询数据库，查到了修改前的旧数据，放到了缓存中。随后数据变更的程序完成了数据库的修改。完了，数据库和缓存中的数据不一样了...

为什么上亿流量高并发场景下，缓存会出现这个问题？

只有在对一个数据在并发的进行读写的时候，才可能会出现这种问题。其实如果说你的并发量很低的话，特别是读并发很低，每天访问量就 1 万次，那么很少的情况下，会出现刚才描述的那种不一致的场景。但是问题是，如果每天的是上亿的流量，每秒并发读是几万，每秒只要有数据更新的请求，就可能会出现上述的数据库+缓存不一致的情况。

解决方案如下：

更新数据的时候，根据数据的唯一标识，将操作路由之后，发送到一个 jvm 内部队列中。读取数据的时候，如果发现数据不在缓存中，那么将重新执行“读取数据+更新缓存”的操作，根据唯一标识路由之后，也发送到同一个 jvm 内部队列中。

一个队列对应一个工作线程，每个工作线程串行拿到对应的操作，然后一条一条的执行。这样的话，一个数据变更的操作，先删除缓存，然后再去更新数据库，但是还没完成更新。此时如果一个读请求过来，没有读到缓存，那么可以先将缓存更新的请求发送到队列中，此时会在队列中积压，然后同步等待缓存更新完成。

这里有一个优化点，一个队列中，其实多个更新缓存请求串在一起是没意义的，因此可以做过滤，如果发现队列中已经有一个更新缓存的请求了，那么就不用再放个更新请求操作进去了，直接等待前面的更新操作请求完成即可。

待那个队列对应的工作线程完成了上一个操作的数据库的修改之后，才会去执行下一个操作，也就是缓存更新的操作，此时会从数据库中读取最新的值，然后写入缓存中。

如果请求还在等待时间范围内，不断轮询发现可以取到值了，那么就直接返回；如果请求等待的时间超过一定时长，那么这一次直接从数据库中读取当前的旧值。

## 高并发的场景下，该解决方案要注意的问题：

**读请求长时阻塞**

由于读请求进行了非常轻度的异步化，所以一定要注意读超时的问题，每个读请求必须在超时时间范围内返回。

该解决方案，最大的风险点在于说，可能数据更新很频繁，导致队列中积压了大量更新操作在里面，然后读请求会发生大量的超时，最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试，看看更新数据的频率是怎样的。

**读请求并发量过高**

这里还必须做好压力测试，确保恰巧碰上上述情况的时候，还有一个风险，就是突然间大量读请求会在几十毫秒的延时 hang 在服务上，看服务能不能扛的住，需要多少机器才能扛住最大的极限情况的峰值。

但是因为并不是所有的数据都在同一时间更新，缓存也不会同一时间失效，所以每次可能也就是少数数据的缓存失效了，然后那些数据对应的读请求过来，并发量应该也不会特别大。

**多服务实例部署的请求路由**

可能这个服务部署了多个实例，那么必须保证说，执行数据更新操作，以及执行缓存更新操作的请求，都通过 Nginx 服务器路由到相同的服务实例上。

比如说，对同一个商品的读写请求，全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由，也可以用 Nginx 的 hash 路由功能等等。

**热点商品的路由问题，导致请求的倾斜**

万一某个商品的读写请求特别高，全部打到相同的机器的相同的队列里面去了，可能会造成某台机器的压力过大。就是说，因为只有在商品数据更新的时候才会清空缓存，然后才会导致读写并发，所以其实要根据业务系统去看，如果更新频率不是太高的话，这个问题的影响并不是特别大，但是的确可能某些机器的负载会高一些

# RDB原理再理解

https://cgiirw.github.io/2018/09/13/Redis_RDB/

## rdbSave
保存是rdbSave()提供的，这个方法有点长，但并不复杂，主要功能如下：

创建临时文件，保存的过程中会先创建一个临时文件；

写入文件前的准备工作，包括初始化IO、设置校验、写入版本号等；

遍历数据库，写入临时文件；

将临时文件覆盖原RDB文件；

做好“善后工作”，比如考虑程序的鲁棒性，对各种可能情况设置好处理方法，并且保存/清除各种需要/不需要的数据，关闭打开的文件等等

在Redis中，可能调用rdbSave的命令包括flushallCommand、debugCommand、rdbSaveBackground、saveCommand、prepareForShutdown。下面就来研究直接保存和后台保存命令的实现。

## save && bgsave

首先看看save，Redis会调用saveCommand函数，它的全部代码：

```c
void saveCommand(redisClient *c) {
    // BGSAVE 已经在执行中，不能再执行 SAVE
    // 否则将产生竞争条件
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    // 执行
    if (rdbSave(server.rdb_filename) == REDIS_OK) {
        addReply(c,shared.ok);
    } else {
        addReply(c,shared.err);
    }
}
```

所以save不是任何时刻都可以执行成功，server.rdb_child_pid不等于-1说明存在执行bgsave的子进程，此时saveCommand()会直接返回。

从上面的代码可以看出，saveCommand()基本上就是简单封装了rdbSave函数。

## bgsave

再来看看bgsave，Redis会调用bgsaveCommand函数：

```c
void bgsaveCommand(redisClient *c) {
    // 不能重复执行 BGSAVE
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
    // 不能在 BGREWRITEAOF 正在运行时执行
    } else if (server.aof_child_pid != -1) {
        addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
    // 执行 BGSAVE
    } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}
```

不能重复执行，也不能在aof子进程存在的时候执行。具体核心代码又封装在rdbSaveBackground()中
```c
int rdbSaveBackground(char *filename) {
    pid_t childpid;
    long long start;
    if (server.rdb_child_pid != -1) return REDIS_ERR;
    // 记录后台保存前服务器状态
    server.dirty_before_bgsave = server.dirty;
    // 开始时间
    start = ustime();
    // 创建子进程
    if ((childpid = fork()) == 0) {
        int retval;
        /* Child */
        // 子进程不接收网络数据
        if (server.ipfd > 0) close(server.ipfd);
        if (server.sofd > 0) close(server.sofd);
        // 保存数据
        retval = rdbSave(filename);
        if (retval == REDIS_OK) {
            size_t private_dirty = zmalloc_get_private_dirty();
            if (private_dirty) {
                redisLog(REDIS_NOTICE,
                    "RDB: %lu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
        }
        // 退出子进程
        exitFromChild((retval == REDIS_OK) ? 0 : 1);
    } else {
        /* Parent */
        // 记录最后一次 fork 的时间
        server.stat_fork_time = ustime()-start;
        // 创建子进程失败时进行错误报告
        if (childpid == -1) {
            redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
        // 记录保存开始的时间
        server.rdb_save_time_start = time(NULL);
        // 记录子进程的 id
        server.rdb_child_pid = childpid;
        // 在执行时关闭对数据库的 rehash
        // 避免 copy-on-write
        updateDictResizePolicy();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}
```

调用fork()会创建新的进程 **(fork创建进程,是把进程完全拷贝一份)**，有三种情况：

fork()如果创建子进程成功，会返回0给子进程；
fork()如果创建子进程成功，会返回子进程的pid号给父进程；
fork()执行发生错误，会返回-1(此时创建新的进程失败，也就无所谓父进程子进程，返回给当前进程)；

一旦childpid = fork()创建子进程成功，则从这个地方开始，将有父子两个进程同时向下按各自逻辑执行代码，下面考虑这种情况下的父子进程的情况：

—–子进程—–

对子进程来说，由于fork()返回了0给它，它会执行从上向下数第二个if语句的内容，断掉该进程的网络连接进而不接受新进的数据，调用rdbSave()进行存储，当它执行到exitFromChild()，内部通过_exit()就结束进程。

—–父进程—–

由于fork()会返回子进程的pid给父进程，所以父进程不会执行第二个if语句中的内容，它会向下执行else中的内容，进行一些日志记录啦，打印一些可能发生的错误报告啦等等，最后返回REDIS_OK。

Redis通过RDB进行持久化的保存功能基本解析完毕了，现在的问题是，bgsave命令执行后，后台保存是一个间断性实施的过程，这个功能如何实现的？

## 服务器的dirty

server.dirty用来记录更改过却没有被持久化的键的个数。

dbSaveBackground()在子进程创建之前执行server.dirty_before_bgsave = server.dirty;，是因为一旦子进程创建之后，由于dirty在父进程和子进程中都会被更改，而它更改后的值分别保存在各自进程拷贝的内存页中，所以要在之前记录一个dirty的锚点，方便后续使用。在之前的文章中，本人提到了操作系统的写时复制(COW)，Redis在这方面利用了操作系统的写时复制机制，下一篇文章将进一步阐述原理，这里只需要暂时知道，在dirty更改前，两个进程的dirty指针指向同一内存地址，一旦变更，那么父进程的dirty不再是子进程的dirty了。

## rdbLoad

Redis的main()会调用loadDataFromDisk()，而loadDataFromDisk()会调用rdbLoad，调用方式如下：

```c
void loadDataFromDisk(void) {
    long long start = ustime();
    // 如果开启了 AOF 功能，那么优先使用 AOF 文件来还原数据
    if (server.aof_state == REDIS_AOF_ON) {
        if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK)
            redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    } else {
        // 在没有开启 AOF 功能时，才使用 RDB 来还原
        if (rdbLoad(server.rdb_filename) == REDIS_OK) {
            redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);
        } else if (errno != ENOENT) {
            redisLog(REDIS_WARNING,"Fatal error loading the DB. Exiting.");
            exit(1);
        }
    }
}
```

RDB在后台保存时会利用操作系统写时复制机制，不至于在内存中完整复制一份要保存的数据，从而减少开销，

# lua脚本保证操作原子性
只满足了原子性的一个特性,执行完成的话,能保证全部执行.但不能保证在执行错误的情况下回滚.

redis的lua脚本类似于MULTI和EXEC,redis保证其中的命令的执行期间,不会插入其他的命令.

# 雪花算法

Twitter-Snowflake算法产生的背景相当简单，为了满足Twitter每秒上万条消息的请求，每条消息都必须分配一条唯一的id，这些id还需要一些大致的顺序（方便客户端排序），并且在分布式系统中不同机器产生的id必须不同。

<div align="center"> <img src=" .\pictures\redis\Snipaste_2019-09-21_12-12-20.jpg" width="600px"> </div><br>

1) 1位，不用。二进制中最高位为1的都是负数，但是我们生成的id一般都使用整数，所以这个最高位固定是0

2) 41位，用来记录时间戳（毫秒）。

3) 41位可以表示2^41−1个数字，如果只用来表示正整数（计算机中正数包含0），可以表示的数值范围是：0 至 2^41−1，减1是因为可表示的数值范围是从0开始算的，而不是1。
也就是说41位可以表示2^41−1个毫秒的值，转化成单位年则是(2^41−1)/(1000∗60∗60∗24∗365)=69年

4) 10位，用来记录工作机器id。
可以部署在2^10=1024个节点，包括5位datacenterId和5位workerId,5位（bit）可以表示的最大正整数是2^5−1=31，即可以用0、1、2、3、....31这32个数字，来表示不同的datecenterId或workerId

1) 12位，序列号，用来记录同毫秒内产生的不同id。
12位（bit）可以表示的最大正整数是2^12−1=4095，即可以用0、1、2、3、....4094这4095个数字，来表示同一机器同一时间截（毫秒)内产生的4095个ID序号

## SnowFlake作用

由于在Java中64bit的整数是long类型，所以在Java中SnowFlake算法生成的id就是long来存储的。

所有生成的id按时间趋势递增

整个分布式系统内不会产生重复id（因为有datacenterId和workerId来做区分）

代码:
```java
public class SnowFlake {

    /**
     * 起始的时间戳:这个时间戳自己随意获取，比如自己代码的时间戳
     */
    private final static long START_STMP = 1543903501000L;

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;  //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

    /**
     * 每一部分的最大值：先进行左移运算，再同-1进行异或运算；异或：相同位置相同结果为0，不同结果为1
     */
     /** 用位运算计算出最大支持的数据中心数量：31 */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);

    /** 用位运算计算出最大支持的机器数量：31 */
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);

    /** 用位运算计算出12位能存储的最大正整数：4095 */
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */

     /** 机器标志较序列号的偏移量 */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;

    /** 数据中心较机器标志的偏移量 */
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;

    /** 时间戳较数据中心的偏移量 */
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private static long datacenterId;  //数据中心
    private static long machineId;    //机器标识
    private static long sequence = 0L; //序列号
    private static long lastStmp = -1L;//上一次时间戳

	 /** 此处无参构造私有，同时没有给出有参构造，在于避免以下两点问题：
	 	  1、私有化避免了通过new的方式进行调用，主要是解决了在for循环中通过new的方式调用产生的id不一定唯一问题问题，因为用于			 记录上一次时间戳的lastStmp永远无法得到比对；
	 	  2、没有给出有参构造在第一点的基础上考虑了一套分布式系统产生的唯一序列号应该是基于相同的参数
	  */
    private SnowFlake(){}

    /**
     * 产生下一个ID
     *
     * @return
     */
    public static synchronized long nextId() {
    	  /** 获取当前时间戳 */
        long currStmp = getNewstmp();

        /** 如果当前时间戳小于上次时间戳则抛出异常 */
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
		 /** 相同毫秒内 */
        if (currStmp == lastStmp) {
            //相同毫秒内，序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {

            		/** 获取下一时间的时间戳并赋值给当前时间戳 */
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内，序列号置为0
            sequence = 0L;
        }
		 /** 当前时间戳存档记录，用于下次产生id时对比是否为相同时间戳 */
        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT      //数据中心部分
                | machineId << MACHINE_LEFT            //机器标识部分
                | sequence;                            //序列号部分
    }

    private static long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private static long getNewstmp() {
        return System.currentTimeMillis();
    }

}
```
## 雪花中的溢出问题
当同一毫秒产生的序列号已经最大了,那就获取下一时间戳,来重新生成id;
## 雪花中的冬令时问题(时钟回拨问题)
我们所说的夏令时实际上包括两类：夏令时和冬令时

```
夏令时(1:00 -> 3:00 AM)
往后拨一个小时，直接从1点变到3点，也就是说我们要比原来提前一个小时和美国人开会。
冬令时(1:00 -> 1:00 -> 2:00 AM)
往前拨一个小时，要过两个1点，这时比平常晚一个小时。
```

由此可见夏令时从1点跳到3点在雪花算法中没有什么影响，但是在冬令时要经历两个相同的时间段并使用相同的时间戳和算法参数进行运算就要出问题了。
解决方法:
备份之前未使用的雪花id,分配给后来的机器用.
https://www.jianshu.com/p/98c202f64652

# 面试题

## 修改配置不重启Redis会实时生效吗？
针对运行实例，有许多配置选项可以通过 CONFIG SET 命令进行修改，而无需执行任何形式的重启。 从 Redis 2.2 开始，可以从 AOF 切换到 RDB 的快照持久性或其他方式而不需要重启 Redis。检索 ‘CONFIG GET *’ 命令获取更多信息。

但偶尔重新启动是必须的，如为升级 Redis 程序到新的版本，或者当你需要修改某些目前 CONFIG 命令还不支持的配置参数的时候。

## Redis的内存用完了会发生什么？

如果达到设置的上限，Redis的写命令会返回错误信息（但是读命令还可以正常返回。）或者你可以将Redis当缓存来使用配置淘汰机制，当Redis达到内存上限时会冲刷掉旧的内容。

## redis常见性能问题和解决方案：
(1) Master最好不要写内存快照，如果Master写内存快照，save命令调度rdbSave函数，会阻塞主线程的工作，当快照比较大时对性能影响是非常大的，会间断性暂停服务。

(2) 如果数据比较重要，某个Slave开启AOF备份数据，策略设置为每秒同步一次

(3) 为了主从复制的速度和连接的稳定性，Master和Slave最好在同一个局域网内

(4) 尽量避免在压力很大的主库上增加从库

(5) 主从复制不要用图状结构，用单向链表结构更为稳定，即：Master <- Slave1 <- Slave2 <- Slave3...这样的结构方便解决单点故障问题，实现Slave对Master的替换。如果Master挂了，可以立刻启用Slave1做Master，其他不变。

<file_sep>/notes/面向对象思想.md
<!-- TOC -->

- [类之间的五大关系](#类之间的五大关系)
    - [继承(继承)](#继承继承)
    - [依赖(方法参数)](#依赖方法参数)
    - [关联(属性)](#关联属性)
    - [聚合()](#聚合)
    - [组合](#组合)
    - [区别](#区别)

<!-- /TOC -->

# 类之间的五大关系
继承(实现)、依赖、关联、聚合、组合
## 继承(继承)
对于类来说，这种关系叫做继承，对于接口来说，这种关系叫做实现
## 依赖(方法参数)
依赖简单的理解，就是一个类A中的方法参数使用到了另一个类B。

这种使用关系是具有偶然性的、临时性的、非常弱的，但是B类的变化会影响到A。

比如说，我用笔写字，首先需要一个类来代表我自己，然后需要一个类来代表一支笔，最后，‘我’要调用‘笔’里的方法来写字，用代码实现一下：

```java
public class Pen {
    public void write(){
        System.out.println("use pen to write");
    }
}

public class Me {
    public void write(Pen pen){//这里，pen作为Me类方法的参数
        pen.write();
    }
}
```
## 关联(属性)

关联体现的是两个类、或者类与接口之间语义级别的一种强依赖关系。

被关联类B以类属性的形式出现在关联类A中，或者关联类A引用了一个类型为被关联类B的全局变量的这种关系，就叫关联关系。

这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的，一般是长期性的，而且双方的关系一般是平等的、关联可以是单向、双向的。

看下面这段代码：

```java
// pen 还是上面的pen
public class You {
    private Pen pen; // 让pen成为you的类属性

    public You(Pen p){
        this.pen = p;
    }

    public void write(){
        pen.write();
    }
}
```

## 聚合()

```java
public class Family {
    private List<Child> children; //一个家庭里有许多孩子

    // ...
}
```
在代码层面，聚合和关联关系是一致的，只能从语义级别来区分。普通的关联关系中，a类和b类没有必然的联系，而聚合中，需要b类是a类的一部分,两个类处在不同的层次上，是一种”has-a“的关系，即 a has-a b; 比如家庭有孩子，屋子里有空调。

但是，has 不是 must has，a可以有b，也可以没有。a是整体，b是部分，整体与部分之间是可分离的，他们可以具有各自的生命周期，部分可以属于多个整体对象，也可以为多个整体对象共享。

## 组合

组合也是关联关系的一种特例，他体现的是一种contains-a的关系，这种关系比聚合更强，也称为强聚合。
```java
public class Nose {
    private Eye eye = new Eye();  //一个人有鼻子有眼睛
    private Nose nose = new Nose();

    // ....
}
```

组合同样体现整体与部分间的关系，但此时整体与部分是不可分的，整体的生命周期结束也就意味着部分的生命周期结束。

就像你有鼻子有眼睛，如果你一不小心结束了生命周期，鼻子和眼睛的生命周期也会结束，而且，鼻子和眼睛不能脱离你单独存在。

只看代码，你是无法区分关联，聚合和组合的，具体是哪一种关系，只能从语义级别来区分。

## 区别

聚合和组合的区别：聚合是个体离开了整体，依然可以存在.组合是个体和整体不可以分开，个体不能离开整体单独存在.

依赖，关联 和聚合，组合的区别:
依赖，关联 : 类之间的关系是在同一层次上.
聚合，组合: 类之间的关系表现为整体和部分.

<file_sep>/notes/SpringBoot.md
<!-- TOC -->

- [切面编程，Aspect Oriented Programming](#%e5%88%87%e9%9d%a2%e7%bc%96%e7%a8%8baspect-oriented-programming)
  - [spring定义](#spring%e5%ae%9a%e4%b9%89)
  - [AOP例子](#aop%e4%be%8b%e5%ad%90)
- [依赖注入与控制反转](#%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5%e4%b8%8e%e6%8e%a7%e5%88%b6%e5%8f%8d%e8%bd%ac)
  - [控制反转介绍](#%e6%8e%a7%e5%88%b6%e5%8f%8d%e8%bd%ac%e4%bb%8b%e7%bb%8d)
  - [IoC能做什么](#ioc%e8%83%bd%e5%81%9a%e4%bb%80%e4%b9%88)
  - [依赖注入（DI，DI—Dependency Injection）](#%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5dididependency-injection)
  - [控制反转与依赖注入的理解](#%e6%8e%a7%e5%88%b6%e5%8f%8d%e8%bd%ac%e4%b8%8e%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5%e7%9a%84%e7%90%86%e8%a7%a3)
- [切面编程实现原理:发射与动态代理](#%e5%88%87%e9%9d%a2%e7%bc%96%e7%a8%8b%e5%ae%9e%e7%8e%b0%e5%8e%9f%e7%90%86%e5%8f%91%e5%b0%84%e4%b8%8e%e5%8a%a8%e6%80%81%e4%bb%a3%e7%90%86)
- [常用注解](#%e5%b8%b8%e7%94%a8%e6%b3%a8%e8%a7%a3)
  - [@AutoWired](#autowired)
- [springboot热部署的两种方式](#springboot%e7%83%ad%e9%83%a8%e7%bd%b2%e7%9a%84%e4%b8%a4%e7%a7%8d%e6%96%b9%e5%bc%8f)
- [spring mvc的执行流程](#spring-mvc%e7%9a%84%e6%89%a7%e8%a1%8c%e6%b5%81%e7%a8%8b)
  - [组件说明：](#%e7%bb%84%e4%bb%b6%e8%af%b4%e6%98%8e)
  - [SpringMVC执行流程:](#springmvc%e6%89%a7%e8%a1%8c%e6%b5%81%e7%a8%8b)

<!-- /TOC -->

# SpringMVC的流程

https://www.jianshu.com/p/8a20c547e245

**SpringMVC执行流程:**
 1.用户发送请求至前端控制器DispatcherServlet
 2.DispatcherServlet收到请求调用处理器映射器HandlerMapping。
 3.处理器映射器根据请求url找到具体的处理器，生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。
 4.DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter执行HandlerAdapter处理一系列的操作，如：参数封装，数据格式转换，数据验证等操作
 5.执行处理器Handler(Controller，也叫页面控制器)。
 6.Handler执行完成返回ModelAndView
 7.HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet
 8.DispatcherServlet将ModelAndView传给ViewReslover视图解析器
 9.ViewReslover解析后返回具体View
 10.DispatcherServlet对View进行渲染视图（即将模型数据model填充至视图中）。
 11.DispatcherServlet响应用户。

## 组件说明：

1.DispatcherServlet：前端控制器。用户请求到达前端控制器，它就相当于mvc模式中的c，dispatcherServlet是整个流程控制的中心，由它调用其它组件处理用户的请求，dispatcherServlet的存在降低了组件之间的耦合性,系统扩展性提高。由框架实现
 2.HandlerMapping：处理器映射器。HandlerMapping负责根据用户请求的url找到Handler即处理器，springmvc提供了不同的映射器实现不同的映射方式，根据一定的规则去查找,例如：xml配置方式，实现接口方式，注解方式等。由框架实现
 3.Handler：处理器。Handler 是继DispatcherServlet前端控制器的后端控制器，在DispatcherServlet的控制下Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求，所以一般情况需要程序员根据业务需求开发Handler。
 4.HandlAdapter：处理器适配器。通过HandlerAdapter对处理器进行执行，这是适配器模式的应用，通过扩展适配器可以对更多类型的处理器进行执行。由框架实现。
 5.ModelAndView是springmvc的封装对象，将model和view封装在一起。
 6.ViewResolver：视图解析器。ViewResolver负责将处理结果生成View视图，ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址，再生成View视图对象，最后对View进行渲染将处理结果通过页面展示给用户。
 7View:是springmvc的封装对象，是一个接口, springmvc框架提供了很多的View视图类型，包括：jspview，pdfview,jstlView、freemarkerView、pdfView等。一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户，需要由程序员根据业务需求开发具体的页面。

# 切面编程，Aspect Oriented Programming

## spring定义

从功能的角度来定义的，
从本质意义上来讲，Spring是一个库，一个Java库，，它的功能是提供了一个软件框架，这个框架目的是使软件之间的逻辑更加清晰，配置更灵活，实现这个目的的手段使用AOP和IoC，而AOP和IoC是一种思想,AOP在Java里是利用反射机制实现（你也可以认为是动态代理，不过动态代理也是反射机制实现的，所以还是先不要管动态代理

如下图所示,去银行取款或者查询余额,都要经过一些类似的步骤,我们要把相同的步骤集中起来管理,当作一个个切面;把不同的部分当来进行灵活更换的组件.

<div align="center"> <img src=".\pictures\spring-boot\Snipaste_2019-09-12_00-02-25.jpg " width="250px"> </div><br>

没有想过可以把这个验证用户的代码是提取出来，不放到主流程里去呢，这就是AOP的作用了，有了AOP，你写代码时不要把这个验证用户步骤写进去，即完全不考虑验证用户，你写完之后，在另我一个地方，写好验证用户的代码，然后告诉Spring你要把这段代码加到哪几个地方，Spring就会帮你加过去，而不要你自己Copy过去，这里还是两个地方，如果你有多个控制流呢，这个写代码的方法可以大大减少你的时间

不过AOP的目的不是这样，这只是一个“副作用”，真正目的是，你写代码的时候，事先只需考虑主流程，而不用考虑那些不重要的流程.因为在主流程写好之后,可以用aop把不重要的代码加到主要流程的前后,不会影响源码,但会生成主流程前后都存在通用功能切面的机器码.

##

## AOP例子
[参考博客](http://www.blogjava.net/javadragon/archive/2006/12/03/85115.html)

参考视频教程:https://www.bilibili.com/video/BV1KT4y1G7hs?from=search&seid=16047420821874007149

1.注解定义一个pointCut,

2.在pointCut那里实现一个方法,该方法的的触发时间由时间注解决定:

3.时间注解: @before @after @环绕  @return @Exception

<img src="pictures/SpringBoot/image-20200417190728179.png" alt="image-20200417190728179" style="zoom:50%;" />

# 依赖注入与控制反转
## 控制反转介绍

Ioc—Inversion of Control，即“控制反转”，不是什么技术，而是一种设计思想。在Java开发中，Ioc意味着将你设计好的对象交给容器控制，而不是传统的在你的对象内部直接控制。

谁控制谁，控制什么：传统Java SE程序设计，我们直接在对象内部通过new进行创建对象，是程序主动去创建依赖对象；而IoC是有专门一个容器来创建这些对象，即由Ioc容器来控制对 象的创建；谁控制谁？当然是IoC 容器控制了对象；控制什么？那就是主要控制了外部资源获取（不只是对象包括比如文件等）。

为何是反转，哪些方面反转了：有反转就有正转，传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象，也就是正转；而反转则是由容器来帮忙创建及注入依赖对象；为何是反转？因为由容器帮我们查找及注入依赖对象，对象只是被动的接受依赖对象，所以是反转；哪些方面反转了？依赖对象的获取被反转了。

## IoC能做什么

IoC 不是一种技术，只是一种思想，一个重要的面向对象编程的法则，它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象，从而导致类与类之间高耦合，难于测试；有了IoC容器后，把创建和查找依赖对象的控制权交给了容器，由容器进行注入组合对象，所以对象与对象之间是 松散耦合，这样也方便测试，利于功能复用，更重要的是使得程序的整个体系结构变得非常灵活。

　　其实IoC对编程带来的最大改变不是从代码上，而是从思想上，发生了“主从换位”的变化。应用程序原本是老大，要获取什么资源都是主动出击，但是在IoC/DI思想中，应用程序就变成被动的了，被动的等待IoC容器来创建并注入它所需要的资源了。

　　IoC很好的体现了面向对象设计法则之一—— 好莱坞法则：“别找我们，我们找你”；即由IoC容器帮对象找相应的依赖对象并注入，而不是由对象主动去找。

## 依赖注入（DI，DI—Dependency Injection）

即“依赖注入”：组件之间依赖关系由容器在运行期决定，形象的说，即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能，而是为了提升组件重用的频率，并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制，我们只需要通过简单的配置，而无需任何代码就可指定目标需要的资源，完成自身的业务逻辑，而不需要关心具体的资源来自何处，由谁实现。

　　理解DI的关键是：“谁依赖谁，为什么需要依赖，谁注入谁，注入了什么”，那我们来深入分析一下：

+ 谁依赖于谁：当然是应用程序依赖于IoC容器；

+ 为什么需要依赖：应用程序需要IoC容器来提供对象需要的外部资源；

+ 谁注入谁：很明显是IoC容器注入应用程序某个对象，应用程序依赖的对象；

+ 注入了什么：**就是注入某个对象所需要的外部资源（包括对象、资源、常量数据）**。

　　IoC和DI由什么关系呢？其实它们是同一个概念的不同角度描述，由于控制反转概念比较含糊（可能只是理解为容器控制对象这一个层面，很难让人想到谁来维护对象关系），所以2004年大师级人物<NAME>又给出了一个新的名字：“依赖注入”，相对IoC 而言，“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。

**依赖注入是由反射实现的**

## 控制反转与依赖注入的理解
在平时的java应用开发中，我们要实现某一个功能或者说是完成某个业务逻辑时至少需要两个或以上的对象来协作完成，在没有使用Spring的时候，每个对象在需要使用他的合作对象时，自己均要使用像new object() 这样的语法来将合作对象创建出来，这个合作对象是由自己主动创建出来的，创建合作对象的主动权在自己手上，自己需要哪个合作对象，就主动去创建，创建合作对象的主动权和创建时机是由自己把控的，而这样就会使得对象间的耦合度高了，A对象需要使用合作对象B来共同完成一件事，A要使用B，那么A就对B产生了依赖，也就是A和B之间存在一种耦合关系，并且是紧密耦合在一起，而使用了Spring之后就不一样了，创建合作对象B的工作是由Spring来做的，Spring创建好B对象，然后存储到一个容器里面，当A对象需要使用B对象时，Spring就从存放对象的那个容器里面取出A要使用的那个B对象，然后交给A对象使用，至于Spring是如何创建那个对象，以及什么时候创建好对象的，A对象不需要关心这些细节问题(你是什么时候生的，怎么生出来的我可不关心，能帮我干活就行)，A得到Spring给我们的对象之后，两个人一起协作完成要完成的工作即可。

　　**所以控制反转IoC(Inversion of Control)是说创建对象的控制权进行转移**，以前创建对象的主动权和创建时机是由自己把控的，而现在这种权力转移到第三方，比如转移交给了IoC容器，它就是一个专门用来创建对象的工厂，你要什么对象，它就给你什么对象，有了 IoC容器，依赖关系就变了，原先的依赖关系就没了，它们都依赖IoC容器了，通过IoC容器来建立它们之间的关系。

　　这是我对Spring的IoC(控制反转)的理解。DI(依赖注入)其实就是IOC的另外一种说法，DI是由<NAME> 在2004年初的一篇论文中首次提出的。他总结：控制的什么被反转了？就是：获得依赖对象的方式反转了

# 切面编程实现原理:发射与动态代理

proxy

# 常用注解

## @AutoWired
作用:@Autowired表示被修饰的类需要注入对象,spring会扫描所有被@Autowired标注的类,然后根据 类型 在ioc容器中找到匹配的类注入。

# springboot热部署的两种方式
https://blog.csdn.net/a656678879/article/details/80356323

# spring mvc的执行流程

## 组件说明：
DispatcherServlet：前端控制器。用户请求到达前端控制器，它就相当于MVC模式中的C，DispatcherServlet是整个流程控制的中心，由它调用其它组件处理用户的请求，DispatcherServlet的存在降低了组件之间的耦合性,系统扩展性提高。由框架实现

HandlerMapping：处理器映射器。HandlerMapping负责根据用户请求的url找到Handler即处理器，Spring MVC提供了不同的映射器实现不同的映射方式，根据一定的规则去查找,例如：xml配置方式，实现接口方式，注解方式等。由框架实现

Handler：处理器。Handler 是继DispatcherServlet前端控制器的后端控制器，在DispatcherServlet的控制下Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求，所以一般情况需要程序员根据业务需求开发Handler。

HandlAdapter：处理器适配器。通过HandlerAdapter对处理器进行执行，这是适配器模式的应用，通过扩展适配器可以对更多类型的处理器进行执行。由框架实现。

ModelAndView是Spring MVC的封装对象，将Model和View封装在一起。

ViewResolver：视图解析器。ViewResolver负责将处理结果生成View视图，ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址，再生成View视图对象，最后对View进行渲染将处理结果通过页面展示给用户。

View:是Spring MVC的封装对象，是一个接口, Spring MVC框架提供了很多的View视图类型，包括：jspview，pdfview,jstlView、freemarkerView、pdfView等。一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户，需要由程序员根据业务需求开发具体的页面。

## SpringMVC执行流程:

1.用户发送请求至前端控制器DispatcherServlet

2.DispatcherServlet收到请求调用处理器映射器HandlerMapping。

3.处理器映射器根据请求url找到具体的处理器，生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。

4.DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter执行HandlerAdapter处理一系列的操作，如：参数封装，数据格式转换，数据验证等操作

5.执行处理器Handler(Controller，也叫页面控制器)。

6.Handler执行完成返回ModelAndView

7.HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet

8.DispatcherServlet将ModelAndView传给ViewReslover视图解析器

9.ViewReslover解析后返回具体View

10.DispatcherServlet对View进行渲染视图（即将模型数据model填充至视图中）。

11.DispatcherServlet响应用户。

# spring cloud使用Feign实现远程接口的调用

在开发中，我们常用httpClient去远程调用其他系统的接口，一般情况下，需要我们指定调用的url，feign也实现了一套远程调用的方法，并且更为优雅。
1.添加依赖

```
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
```

2.创建FeignClient接口（用于指定远程调用的服务）

2.创建FeignClient接口（用于指定远程调用的服务）

```
// 申明这是一个Feign客户端，并且指明服务id
@FeignClient(value = "com-spring-caclulate")
public interface CacluFeignClient {

 // 这里定义了类似于SpringMVC用法的方法，就可以进行RESTful的调用了
@RequestMapping(value = "/caclu/{num}", method = RequestMethod.GET)
public Item caclulate(@PathVariable("num") Integer num);

}
```

注意：这里就是一个接口。
3.在需要进行远程调用的方法里注入该接口，并调用对应的api接口方法

注意：这里就是一个接口。
3.在需要进行远程调用的方法里注入该接口，并调用对应的api接口方法

```
@Autowired
private CacluFeignClient cacluFeignClient ;

@GetMapping(value = "query/result")
public Integer caclulate() {
    cacluFeignClient.caclulate(1);
}
```

4.在启动类上添加注解 @EnableFeignClients，表示支持Feign

FeignClient接口和spring mvc接口的格式一致，在调用方的方法中，我们只需要调用本系统中定义的接口即可。

# bean

## 产生机制

## 作用域

| Scope                                                        | Description                                                  |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| [singleton](https://docs.spring.io/spring/docs/5.1.4.RELEASE/spring-framework-reference/core.html#beans-factory-scopes-singleton) | （默认的）在每个`Spring IoC`容器中，一个`bean`定义对应只会有唯一的一个`bean`实例。 |
| [prototype](https://docs.spring.io/spring/docs/5.1.4.RELEASE/spring-framework-reference/core.html#beans-factory-scopes-prototype) | 一个`bean`定义可以有多个`bean`实例。                         |
| [request](https://docs.spring.io/spring/docs/5.1.4.RELEASE/spring-framework-reference/core.html#beans-factory-scopes-request) | 一个`bean`定义对应于单个`HTTP` 请求的生命周期。也就是说，每个`HTTP` 请求都有一个`bean`实例，且该实例仅在这个`HTTP` 请求的生命周期里有效。该作用域仅适用于`WebApplicationContext`环境。 |
| [session](https://docs.spring.io/spring/docs/5.1.4.RELEASE/spring-framework-reference/core.html#beans-factory-scopes-session) | 一个`bean` 定义对应于单个`HTTP Session` 的生命周期，也就是说，每个`HTTP Session` 都有一个`bean`实例，且该实例仅在这个`HTTP Session` 的生命周期里有效。该作用域仅适用于`WebApplicationContext`环境。 |
| [application](https://docs.spring.io/spring/docs/5.1.4.RELEASE/spring-framework-reference/core.html#beans-factory-scopes-application) | 一个`bean` 定义对应于单个`ServletContext` 的生命周期。该作用域仅适用于`WebApplicationContext`环境。 |
| [websocket](https://docs.spring.io/spring/docs/5.1.4.RELEASE/spring-framework-reference/web.html#websocket-stomp-websocket-scope) | 一个`bean` 定义对应于单个`websocket` 的生命周期。该作用域仅适用于`WebApplicationContext`环境。 |

# 开箱即用,自动配置的原理

Spring Boot 内部提供了很多自动化配置的类这些自动化配置的类会判断 classpath 中是否存在自己需要的那个类，如果存在则会自动配置相关的配置，否则就不会自动配置，因此，开发者在 Maven 的 pom 文件中添加相关依赖后，这些依赖就会下载很多 jar 包到 classpath 中，有了这些 lib 就会触发自动化配置。

pring Boot提供了很多“开箱即用”的依赖模块，都是以spring-boot-starter-xx作为命名的。例如，之前提到的 spring-boot-starter-redis、spring-boot-starter-data-mongodb、spring-boot-starter-data-elasticsearch 等。

Spring Boot 的开箱即用是一个很棒的设计，给开发者带来很大的便利。开发者只要在 Maven 的 pom 文件中添加相关依赖后，Spring Boot 就会针对这个应用自动创建和注入需要的 Spring  Bean 到上下文中。

自动注入的核心在于 spring-boot-autoconfigure.jar 这个类库。在分析之前，我们先来看几个文件。

```
@Configuration
@ConditionalOnClass({ JedisConnection.class, RedisOperations.class, Jedis.class })
@EnableConfigurationProperties
public class RedisAutoConfiguration {}
```

```
@Configuration
@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class,
    NodeClientFactoryBean.class })
@EnableConfigurationProperties(ElasticsearchProperties.class)
public class ElasticsearchAutoConfiguration implements DisposableBean {}
```

上面三个源码分别对应Redis、MongoDB、ElasticSearch。通过对比，我们会发现它们都有一个特点，都存在 @ConditionalOnClass 注解。这个注解就是问题的关键所在。