区别:重载实现于一个类中;重写实现于子类中。
重载(Overload):是一个类中多态性的一种表现,指同一个类中不同的函数使用相同的函数名,但是函数的参数个数或类型不同。可以有不同的返回类型;可以有不同的访问修饰符;可以抛出不同的异常。调用的时候根据函数的参数来区别不同的函数。
重写(Override): 是父类与子类之间的多态性,是子类对父类函数的重新实现。函数名和参数与父类一样,子类与父类函数体内容不一样。子类返回的类型必须与父类保持一致;子类方法访问修饰符的限制一定要大于父类方法的访问修饰(public>protected>default>private);子类重写方法一定不能抛出新的检查异常或者比被父类方法申明更加宽泛的检查型异常。
代码语言:txt复制### 关键字
###### String、StringBuffer和StringBuilder类的区别
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
一、可变与不可变
String类是一个不可变类,即创建String对象后,该对象中的字符串是不可改变的,直到这个对象被销毁。StringBuffer与StringBuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,是可变类。
由于String是可变类,适合在需要被共享的场合中使用,当一个字符串经常被修改时,最好使用StringBuffer实现。如果用String保存一个经常被修改的字符串,该字符串每次修改时都会创建新的无用的对象,这些无用的对象会被垃圾回收器回收,会影响程序的性能,不建议这么做。
二、初始化方式
当创建String对象时,可以利用构造方法String str = new String("Java")的方式来对其进行初始化,也可以直接用赋值的方式String s = "Java"来初始化。而StringBuffer只能使用构造方法StringBuffer sb = new StringBuffer("hello")的方式初始化。
三、字符串修改方式
String字符串修改方法是首先创建一个StringBuffer,其次调用StringBuffer的append方法,最后调用StringBuffer的toString()方法把结果返回,示例代码如下:
四、是否实现了equals和hashCode方法
String实现了equals()方法和hashCode()方法,new String("java").equals(new String("java"))的结果为true;
而StringBuffer没有实现equals()方法和hashCode()方法,因此,new StringBuffer("java").equals(new StringBuffer("java"))的结果为false,将StringBuffer对象存储进Java集合类中会出现问题。
五、是否线程安全
StringBuffer与StringBuilder都提供了一系列插入、追加、改变字符串里的字符序列的方法,它们的用法基本相同,只是StringBuilder是线程不安全的,StringBuffer是线程安全的,。如果只是在单线程中使用字符串缓冲区,则StringBuilder的效率会高些,但是当多线程访问时,最好使用StringBuffer。
综上,在执行效率方面,StringBuilder最高,StringBuffer次之,String最低,对于这种情况,一般而言,如果要操作的数量比较小,应优先使用String类;如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先使用StringBuilder类。
代码语言:txt复制###### final关键字特性
final关键字可以申明成员变量、方法、类、本地变量。一旦将引用声明为final,将无法再改变这个引用,final关键字还能保证内存同步。
final变量:
代码语言:txt复制final变量有成员变量或者是本地变量(方法内的局部变量),在类成员中final经常和static一起使用,作为类常量使用。其中类常量必须在声明时初始化,final成员常量可以在构造函数初始化。
final方法:
代码语言:txt复制final方法表示该方法不能被子类的方法重写,将方法声明为final,在编译的时候就已经静态绑定了,不需要在运行时动态绑定。final方法调用时使用的是invokespecial指令。
final类:
代码语言:txt复制final类不能被继承,final类中的方法默认也会是final类型的,java中的String类和Integer类都是final类型的。
final方法的好处:
代码语言:txt复制提高了性能,JVM在常量池中会缓存final变量
代码语言:txt复制final变量在多线程中并发安全,无需额外的同步开销
代码语言:txt复制final方法是静态编译的,提高了调用速度
代码语言:txt复制final类创建的对象是只可读的,在多线程可以安全共享
final关键字的知识点:
代码语言:txt复制final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。final变量一旦被初始化后不能再次赋值。
代码语言:txt复制本地变量必须在声明时赋值。 因为没有初始化的过程
代码语言:txt复制在匿名类中所有变量都必须是final变量。
代码语言:txt复制final方法不能被重写, final类不能被继承
代码语言:txt复制接口中声明的所有变量本身是final的。类似于匿名类
代码语言:txt复制final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
代码语言:txt复制final方法在编译阶段绑定,称为静态绑定(static binding)。
代码语言:txt复制将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。
代码语言:txt复制### JVM
###### Jvm的内存结构
(线程私有)
1、程序计数器:它可以看作是当前线程所执行的字节码的行号指示器。
2、虚拟机栈:它描述的是java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法。
3、本地方法栈:本地方法栈和虚拟机栈所发挥的作用是很相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
(线程共享)
4、堆:它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用;通过参数-Xms设定初始值、-Xmx设定最大值
5、方法区:方法区是被所有线程共享的内存区域,用来存储已被虚拟机加载的类信息、常量池、静态变量、JIT(just in time,即时编译技术)编译后的代码等数据。
代码语言:txt复制###### Java的堆内存划分
1、堆内存为了配合垃圾回收划分了不同的区域
2、Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。
3、新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。
代码语言:txt复制###### Java中的对象引用
1、强引用:如“Object obj = new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
2、软引用:它用来描述一些可能还有用,但并非必须的对象。在系统内存不够时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用
3、弱引用:它也是用来描述非须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
代码语言:txt复制 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
4、虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
代码语言:txt复制 为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。
代码语言:txt复制###### 垃圾回收算法
1、引用计数算法:堆中的每个对象都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。
代码语言:txt复制 当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。
代码语言:txt复制 任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
代码语言:txt复制 优点:引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利(OC的内存管理使用该算法)。
缺点: 难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。
2、根搜索算法: 通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
代码语言:txt复制 找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
代码语言:txt复制 搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
Java和C#中都是采用根搜索算法来判定对象是否存活的。
代码语言:txt复制###### GC Roots有哪些
(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中的常量引用的对象;
(3)方法区中的类静态属性引用的对象;
(4)本地方法栈中JNI(Native方法)的引用对象。
(5)活跃线程。
代码语言:txt复制###### 回收垃圾对象内存的算法
1、标记—清除法:
2、标记—整理法:
3、复制算法:该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
4、Adaptive算法:在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。
代码语言:txt复制###### 垃圾回收机制(GC)
垃圾回收(Garbage Collection)是Java虚拟机(JVM)提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
1、新生代GC(Minor GC/Scavenge GC):发生在新生代的垃圾收集动作。因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁(不一定等Eden区满了才触发),一般回收速度也比较快。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
2、老年代GC(Major GC/Full GC):发生在老年代的垃圾回收动作。Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
(1)串行垃圾回收器(Serial Garbage Collector):串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。通过JVM参数-XX: UseSerialGC可以使用串行垃圾回收器。
(2)并行垃圾回收器(Parallel Garbage Collector):它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,当执行垃圾回收的时候它也会冻结所有的应用程序线程。适用于多CPU、对暂停时间要求较短的应用上。可用-XX: UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
(3)并发标记扫描垃圾回收器(CMS Garbage Collector):使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。通过JVM参数 XX: USeParNewGC 打开并发标记扫描垃圾回收器。
代码语言:txt复制 并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
代码语言:txt复制 (1)当标记的引用对象在Tenured区域;
代码语言:txt复制 (2)在进行垃圾回收的时候,堆内存的数据被并发的改变。
(4)G1垃圾回收器(G1 Garbage Collector): G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。 G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域。
通过JVM参数 –XX: UseG1GC 使用G1垃圾回收器。
代码语言:txt复制###### GC性能调优
代码语言:txt复制### 集合
![这里写图片描述](https://img-blog.csdn.net/20180803195348216?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZlaXlhbmFmZmVjdGlvbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
#### Collection 集合
>一、Collection 集合接口
>Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素, Java不提供直接继承自Collection的类,只提供继承于的子接口(如List和set)。 Collection 接口存储一组不唯一,无序的对象。
>
>>1、**AbstractCollection**
>>
>>AbstractCollection 是 [Java](https://so.csdn.net/so/search?q=Java&spm=1001.2101.3001.7020) 集合框架中 [Collection 接口](http://blog.csdn.net/u011240877/article/details/52773577) 的一个直接实现类, [Collection](https://so.csdn.net/so/search?q=Collection&spm=1001.2101.3001.7020) 下的大多数子类都继承 AbstractCollection ,比如 List 的实现类, Set的实现类。它实现了一些方法,也定义了几个[抽象方法](https://so.csdn.net/so/search?q=抽象方法&spm=1001.2101.3001.7020)留给子类实现,因此它是一个**抽象类**。
>
>>>1.1、**AbstractList**
>>>
>>>AbstractList 继承自 [AbstractCollection 抽象类](http://blog.csdn.net/u011240877/article/details/52829912),实现了 [List 接口](http://blog.csdn.net/u011240877/article/details/52802849) ,是 ArrayList 和 AbstractSequentiaList 的父类
>
>>>>1.1.1、**AbstractSequentialList**
>>>>
>>>>什么是 AbstractSequentialList( [Sequential](https://so.csdn.net/so/search?q=Sequential&spm=1001.2101.3001.7020) 相继的,按次序的)AbstractSequentialList 没有什么特别的,这里是为了理解 [LinkedList](https://so.csdn.net/so/search?q=LinkedList&spm=1001.2101.3001.7020) 更容易。AbstractSequentialList 继承自 [AbstractList](http://blog.csdn.net/u011240877/article/details/52834074),是 `LinkedList` 的父类,是 [List 接口](http://blog.csdn.net/u011240877/article/details/52802849) 的简化版实现。简化在哪儿呢?简化在 AbstractSequentialList **只支持按次序访问**,而不像 [AbstractList](http://blog.csdn.net/u011240877/article/details/52834074) 那样支持随机访问。想要实现一个支持按次序访问的 [List](https://so.csdn.net/so/search?q=List&spm=1001.2101.3001.7020)的话,只需要继承这个抽象类,然后把指定的抽象方法实现就好了。需要实现的方法:
>
>>>1.2、**AbstractSet**
>>>
>>>AbstractSet没有提供具有AbstractCollection中的所有方法,并且只重写了[equals](https://so.csdn.net/so/search?q=equals&spm=1001.2101.3001.7020),hashCode,removeAll三个方法。
>>
>>
> > **1、List 接口**
> >
> > List接口是一个有序的 Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元素。 List 接口存储一组不唯一,有序(插入顺序)的对象。
> >
> > >1.1、LinkedList
> > >
> > >LinkedList 继承自 [AbstractSequentialList 接口](http://blog.csdn.net/u011240877/article/details/52854681),同时了还实现了 [Deque](http://blog.csdn.net/u011240877/article/details/52865173) 接口。
> > >
> > >我们知道 [ArrayList](https://so.csdn.net/so/search?q=ArrayList&spm=1001.2101.3001.7020) 是以数组实现的,遍历时很快,但是插入、删除时都需要移动后面的元素,效率略差些。
> > >
> > >而LinkedList 是以链表实现的,插入、删除时只需要改变前后两个节点指针指向即可,省事不少。
> >
> > >1.2、**ArrayList**
> > >
> > >ArrayList 是 [Java](https://so.csdn.net/so/search?q=Java&spm=1001.2101.3001.7020) 集合框架中 [List接口](http://blog.csdn.net/u011240877/article/details/52802849) 的一个实现类。可以说 ArrayList 是我们使用最多的 [List](https://so.csdn.net/so/search?q=List&spm=1001.2101.3001.7020) 集合,它有以下特点:
> > >
> > >- 容量不固定,想放多少放多少(当然有最大阈值,但一般达不到)
> > >- 有序的(元素输出顺序与输入顺序一致)
> > >- 元素可以为 null
> > >- 效率高 size(), isEmpty(), get(), set() iterator(), ListIterator() 方法的时间复杂度都是 O(1)
> > >- add() 添加操作的时间复杂度平均为 O(n) 其他所有操作的时间复杂度几乎都是 O(n)
> > >- 占用空间更小 对比 LinkedList,不用占用额外空间维护链表结构
>
> > **2、Set 接口**
> >
> > Set 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素。 Set 接口存储一组无序的、不可重复的对象
>
> >>2.1、SortedSet
> >
> >>继承于Set保存有序的集合。
> >
> >>2.2、[HashSet](https://www.runoob.com/java/java-hashset.html)
> >
> >>- HashSet 允许有 null 值。
> >
> >>- HashSet 是无序的,即不会记录插入的顺序。
> >
> >>- HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 须在多线程访问时显式同步对 HashSet 的并发访问。
> >
> >>- HashSet 实现了 Set 接口。
> >
> >>2.3、**LinkedHashSet**
> >
> >>是HashSet的子类,底层使用了LinkedHashMap,在HashSet的哈希表数据结构基础之上,增加了一个双向链表用来记录元素添加的顺序,能按照添加顺序遍历输出。需要频繁遍历的话效率可能高于HashSet
> >
> >>2.4、 TreeSet
> >
> >>TreeSet是一个有序的集合,它的作用是提供有序的Set集合。它继承了AbstractSet抽象类,实现了NavigableSet<E>,Cloneable,Serializable接口。TreeSet是基于TreeMap实现的,TreeSet的元素支持2种排序方式:自然排序或者根据提供的Comparator进行排序。
> >
> >3、Queue 队列接口
> >
> >Queue 继承自 Collection 接口,Deque, LinkedList, PriorityQueue, BlockingQueue 等类都实现了它。
> >
> >除了继承 Collection 接口的一些方法,Queue 还添加了额外的 添加、删除、查询操作。
> >
> >[Queue](https://so.csdn.net/so/search?q=Queue&spm=1001.2101.3001.7020) 用来存放 等待处理元素 的集合,这种场景一般用于缓冲、并发访问。
> >
> >> 3.1、*Deque*
> >
> >> *Deque* 是 *Double ended queue (双端队列)* 的缩写,读音和 deck 一样,蛋壳。
> >
> >> Deque 继承自 [Queue](http://blog.csdn.net/u011240877/article/details/52860924),直接实现了它的有 ArrayDeque、ConcurrentLinkedDeque、LinkedBlockingDeque、LinkedList 等。
> >
> >> Deque 支持容量受限的双端队列,也支持大小不固定的。一般双端队列大小不确定。
> >
> >> Deque 接口定义了一些从头部和尾部访问元素的方法。比如分别在头部、尾部进行插入、删除、获取元素。
> >
> >> >3.1.1、ArrayDeque
> >> >
> >> >`ArrayDeque`是`Deque`接口的一个实现,使用了可变数组,所以没有容量上的限制。
> >> >
> >> >同时,`ArrayDeque`是线程不安全的,在没有外部同步的情况下,不能再多线程环境下使用。
> >> >
> >> >`ArrayDeque`是`Deque`的实现类,可以作为栈来使用,效率高于`Stack`;
> >> >
> >> >也可以作为队列来使用,效率高于`LinkedList`。
> >> >
> >> >需要注意的是,`ArrayDeque`不支持`null`值。
**BitSet**
Java平台的BitSet用于存放一个位序列,如果要高效的存放一个位序列,就可以使用位集(BitSet)。由于位集将位包装在字节里,所以使用位集比使用Boolean对象的List更加高效和更加节省存储空间。
集合算法
[`集合算法`](https://blog.csdn.net/weixin_33507838/article/details/115000570)
集合框架定义了几种可应用于集合和映射的算法。
这些算法在Collections类中定义为静态方法。 有些方法可能会抛出ClassCastException ,当尝试比较不兼容的类型时会发生这种情况,或者在尝试修改不可修改的集合时发生UnsupportedOperationException 。
代码语言:txt复制 迭代器(lterator)
迭代器,使你能够通过循环来得到或删除集合的元素。ListIterator 继承了 Iterator,以允许双向遍历列表和修改元素。
代码语言:txt复制#### Map集合
>二、 Map
>
>Map 接口存储一组键值对象,提供key(键)到value(值)的映射。
>
>>1、AbstractMap
>
>>AbstractMap 是 [Map 接口的](http://blog.csdn.net/u011240877/article/details/52929523)的实现类之一,也是 **HashMap**, [TreeMap](https://so.csdn.net/so/search?q=TreeMap&spm=1001.2101.3001.7020), **ConcurrentHashMap** 等类的父类。 AbstractMap 提供了 Map 的基本实现,使得我们以后要实现一个 Map 不用从头开始,只需要继承 AbstractMap, 然后按需求实现/重写对应方法即可。
>
>>>1.1、**HashMap**
>
>>>HashMap 是一个采用**哈希表**实现的键值对集合,继承自 [AbstractMap](http://blog.csdn.net/u011240877/article/details/52949046),实现了 [Map 接口](http://blog.csdn.net/u011240877/article/details/52929523) 。
>
>>>HashMap 的特殊存储结构使得在获取指定元素前需要经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量比较即可得到元素,这使得 HashMap 的查找效率贼高。
>
>>>当发生 哈希冲突(碰撞)的时候,HashMap 采用 拉链法 进行解决,因此 HashMap 的底层实现是 数组 链表
>
>>>JDK 1.8 以前 HashMap 的实现是 数组 链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
>
>>>当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
>
>>>针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。
>
>>>**HashMap 的特点**
>
>>>- 底层实现是 链表数组,JDK 8 后又加了 红黑树
>>>- 实现了 Map 全部的方法
>>>- key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类需要重写 hashCode 和 equals 方法
>>>- 允许空键和空值(但空键只有一个,且放在第一位,下面会介绍)
>>>- 元素是无序的,而且顺序会不定时改变
>>>- 插入、获取的时间复杂度基本是 O(1)(前提是有适当的哈希函数,让元素分布在均匀的位置)
>>>- 遍历整个 Map 需要的时间与 桶(数组) 的长度成正比(因此初始化时 HashMap 的容量不宜太大)
>>>- 两个关键因子:初始容量、加载因子
>
>>>1.1.1、**LinkedHashMap**
>
>>>LinkedHashMap继承于HashMap,**迭代HashMap的顺序并不是HashMap放置的顺序**,也就是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。
>
>>>这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是**通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序**。**该迭代顺序可以是插入顺序或者是访问顺序。**
>
>>>1.2、**ConcurrentHashMap**
>>>
>>>**1.2.1、ConcurrentHashMap 1.7**
>>>
>>>其实大体的哈希表实现跟 HashMap 没有本质的区别,都是经过 key 的 hash 定位到一个下标,然后获取元素,如果冲突了就用链表相连。
>>>
>>>差别就在于引入了一个 Segments 数组。
>>>
>>>原理就是先通过 key 的 hash 判断得到 Segment 数组的下标,然后将这个 Segment 上锁,然后再次通过 key 的 hash 得到 Segment 里 HashEntry 数组的下标,下面这步其实就是 HashMap 一致了,所以我说差别就是引入了一个 Segments 数组。
>>>
>>>因此可以简化的这样理解:每个 Segment 数组存放的就是一个单独的 HashMap。
>>>
>>>如果我们有 6 个 Segment ,那么等于有六把锁,因此共可以有六个线程同时操作这个 ConcurrentHashMap,并发度就是 6,相比于直接将 put 方法上锁,并发度就提高了,这就是分段锁。
>>>
>>>具体上锁的方式来源于 Segment ,这个类实际继承了 ReentrantLock,因此它自身具备加锁的功能。
>>>
>>>可以看出,1.7 的分段锁已经有了细化锁粒度的概念,它的一个缺陷是 Segment 数组一旦初始化了之后不会扩容,只有 HashEntry 数组会扩容,这就导致并发度过于死板,不能随着数据的增加而提高并发度。
>>>
>>>**1.2.2、ConcurrentHashMap 1.8**
>>>
>>>1.8 ConcurrentHashMap 做了更细粒度的锁控制,可以理解为 1.8 HashMap 的数组的每个位置都是一把锁,这样扩容了锁也会变多,并发度也会增加
>>>
>>>思想的转变就是把粒度更加细化,我不分段了,我直接把 Node 数组的每个节点分别上一把锁,这样并发度不就更高了吗?
>>>
>>>并且 1.8 也不借助于 ReentrantLock 了,直接用 synchronized,这也侧面证明,都 1.8 了 synchronized 优化后的速度已经不下于 ReentrantLock 了
>>>
>>>具体实现思路也简单,当塞入一个值的时候,先计算 key 的 hash 后的下标,如果计算到的下标还未有 Node ,那么就通过 cas 塞入新的 Node。如果已经有 node 则通过 synchronized 将这个 node 上锁,这样别的线程就无法访问这个 node 及其之后的所有节点。
>>>
>>>然后判断 key 是否相等,相等则是替换 value ,反之则是新增一个 node,这个操作和 HashMap 是一样的。
>>>
>>>###### 1.8 的扩容也是有点意思的,它允许协助扩容,也就是多线程扩容。
>>>
>>>###### 还有 1.8 的 size 方法和 1.7 也不一样
>>>
>>>
>
>>>1.3、**HashTable**
>
>>>与HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可为null。Hashtable中的映射不是有序的。
>
>>>1.4、TreeMap
>
>>>TreeMap提供了一种以**排序顺序存储键/值对**的有效方法。它是基于**红黑树**的NavigableMap实现。继承自 [AbstractMap](http://blog.csdn.net/u011240877/article/details/52949046),实现了NavigableMap接口。
>
>>>1.5、IdentityHashMap
>
>>>IdentityHashMap与HashMap 都是,继承自 [AbstractMap](http://blog.csdn.net/u011240877/article/details/52949046),实现了 [Map 接口](http://blog.csdn.net/u011240877/article/details/52929523) 。不同的是,其比较键(或值)时,使用引用相等性代替对象相等性。
>
>>>1.6、WeakHashMap
>>>
>>>WeakHashMap与正常的HashMap类似,不同点在WeakHashMap的key是**「弱引用」**的key。WeakHashMap具有弱引用的特点:随时被回收对象。 继承自 [AbstractMap](http://blog.csdn.net/u011240877/article/details/52949046),实现了Map接口。
>
>>2.1、Map.Entry的作用
>>
>>Map.Entry是为了更方便的输出map键值对。一般情况下,要输出Map中的key 和 value 是先得到key的集合keySet(),然后再迭代(循环)由每个key得到每个value。values()方法是获取集合中的所有值,不包含键,没有对应关系。而Entry可以一次性获得这两个值。
>
>>2.2、SortedMap
>>继承于 Map,使 Key 保持在升序排列。
>>
>>>2.2.1、NavigableMap
>>>
>>>*NavigableMap*(`java.util.NavigableMap`)接口是[`SortedMap`](https://blog.csdn.net/cgsyck/article/details/108462189)的子接口,但是 NavigableMap接口中新加了几个SortedSet接口中没有的方法,使导航存储在映射中的键和值成为可能。
>
>三、Vector
>
>Vector 类实现了一个动态数组和 ArrayList 很相似,但是该类是同步的,可以用在多线程的情况,该类允许设置默认增长长度,默认扩容方式为原来的2倍。
>
>>1、Stack
>>
>>Stack 继承自 Vector
>>
>>- ##### Stack 栈是一个 "先进后出"的原理
>>
>>- ##### Stack 本质是一个List,其具备 List 所有方法
>四、Dictionary
>
>Dictionary 类是一个抽象类,用来存储键/值对,作用和Map类相似。
>
>> 1、**HashTable**
>>
>> 与HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可为null。Hashtable中的映射不是有序的。
>>
>> >1.1、Properties
>> >
>> >Properties 继承于 Hashtable。表示一个持久的属性集.属性列表中每个键及其对应值都是一个字符串。
###### 说说Java的集合类
Java集合从分类上看,有 collection 和 map 两种,前者是存储对象的集合类,后者存储的是键值对(key-value)
Collection
Set
主要功能是保证存储的集合不会重复,至于集体是有序还是无序的,需要看具体的实现类,比如 TreeSet 就是有序的,HashSet 是无序的(所以网上有些说 set 是无序集合是在扯淡)
List
这个很熟悉,具体的实现类有 ArrayList 和 LinkedList,两者的区别在于底层实现不同,前者是数组,后者是双向链表,所以引申出来就是将数组和链表的区别。
数组 VS 链表
数组
数组的内存是连续的,且存储的元素大小是固定的,实现上是基于一个内存地址,然后由于元素固定大小,支持利用下标的直接访问。
具体是通过下标 * 元素大小 内存基地址算出一个访问地址,然后直接访问,所以随机访问的效率很高,O(1)。
而由于要保持内存连续这个特性,不能在内存中间空一块,所以删除中间元素时就需要搬迁元素,需进行内存拷贝,所以说删除的效率不高。
链表
链表的内存不需要连续,它们是通过指针相连,这样对内存的要求没那么高(数组的申请需要一块连续的内存),链表就可以散装内存,不过链表需要额外存储指针,所以总体来说,链表的占用内存会大一些。
且由于是指针相连,所以直接无法随机访问一个元素,必须从头(如双向链表,可尾部)开始遍历,所以随机查找的效率不高,O(n)。
也由于指针相连这个特性,单方面删除的效率高,因为只需要改变指针即可,没有额外的内存拷贝动作(但是要找到这个元素,费劲儿呀,除非你顺序遍历删)。
两者大致的特点就如上所说,再扯地深一点,就要说到 CPU 亲和性问题
空间局部性(spatial locality):如果一个存储器的位置被引用,那么将来它附近的位置也会被引用。
根据这个原理,就会有预读功能,像 CPU 缓存就会读连续的内存,这样一来如果你本就要遍历数组的,那么你后面的数据就已经被上一次读取前面数据的时候,一块被加载了,这样就是 CPU 亲和性高。
反观链表,由于内存不连续,所以预读不到,所以 CPU 亲和性低。
对了,链表(数组)加了点约束的话,还可以用作栈、队列和双向队列。
像 LinkedList 就可以用来作为栈或者队列使用。
Queue
队列,有序,严格遵守先进先出,就像往常的排队,没啥别的好说的。
常用的实现类就是 LinkedList,没错这玩意还实现了 Queue 接口。
还有一个值得一提的是优先队列,即 PriorityQueue,内部是基于数组构建的,用法就是你自定义一个 comparator ,自己定义对比规则,这个队列就是按这个规则来排列出队的优先级。
代码语言:txt复制###### HashMap 的实现原理吗
其实就是有个 Entry 数组,Entry 保存了 key 和 value。当你要塞入一个键值对的时候,会根据一个 hash 算法计算 key 的 hash 值,然后通过数组大小 n-1 & hash 值之后,得到一个数组的下标,然后往那个位置塞入这个 Entry。
然后我们知道,hash 算法是可能产生冲突的,且数组的大小是有限的,所以很可能通过不同的 key 计算得到一样的下标,因此为了解决 Entry 冲突的问题,采用了链表法。
在 JDK1.7 及之前链表的插入采用的是头插法,即在链表的头部插入新的 Entry。在 JDK1.8 的时候,改成了尾插法,并且引入了红黑树。
代码语言:txt复制###### 那 JDK 1.8 对 HashMap 有哪些改动?
1、hash 函数的优化
2、扩容 rehash 的优化
3、头插法和尾插法
4、插入与扩容时机的变更
5、引入了红黑树
代码语言:txt复制1、hash 函数的优化
1.7是这样实现的:
代码语言:javascript复制static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
而 1.8 是这样实现的:
代码语言:javascript复制static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
具体而言就是 1.7 的操作太多了,经历了四次异或,所以 1.8 优化了下,它将 key 的哈希码的高16位和低16位进行了异或,得到的 hash 值同时拥有了高位和低位的特性,这样做得出的码比较均匀,不容易冲突。
这也是 JDK 开发者根据速度、实用性、哈希质量所做的权衡来做的实现:
代码语言:txt复制2、扩容 rehash 的优化
按照我们的思维,正常扩容肯定是先申请一个更大的数组,然后将原数组里面的每一个元素重新 hash 判断在新数组的位置,然后一个一个搬迁过去。
在 1.7 的时候就是这样实现的,然而 1.8 在这里做了优化,关键点就在于数组的长度是 2 的次方,且扩容为 2 倍。
总结一下,1.8 的扩容不需要每个节点重写 hash 算下标,而是通过和老数组长度的&计算是否为 0 ,来判断新下标的位置。
代码语言:txt复制3、头插法和尾插法
1.7是头插法,头插法的好处就是插入的时候不需要遍历链表,直接替换成头结点,但是缺点是扩容的时候会逆序,而逆序在多线程操作下可能会出现环,然后就死循环了。
然后 1.8 是尾插法,每次都从尾部插入的话,扩容后链表的顺序还是和之前一致,所以不可能出现多线程扩容成环的情况。
代码语言:txt复制4、插入与扩容时机的变更
1.7 是先判断 put 的键值对是新增还是替换,如果是替换则直接替换,如果是新增会判断当前元素数量是否大于等于阈值,如果超过阈值且命中数组索引的位置已经有元素了,那么就进行扩容。所以 1.7 是先扩容,然后再插入。
而 1.8 则是先插入,然后再判断 size 是否大于阈值,若大于则扩容。
代码语言:txt复制5、引入了红黑树
当链表的长度大于 8 且数组大小大于等于 64 的时候,就把链表转化成红黑树,当红黑树节点小于 6 的时候,又会退化成链表。
为什么 JDK 1.8 要对 HashMap 做红黑树这个改动?
主要是避免 hash 冲突导致链表的长度过长,这样 get 的时候时间复杂度严格来说就不是 O(1) 了,因为可能需要遍历链表来查找命中的 Entry。
代码语言:txt复制###### 为什么 HashMap 的长度一定要是 2 的 n 次幂?
原因就在于数组下标的计算,由于下标的计算公式用的是 i = (n - 1) & hash,即位运算,一般我们能想到的是 %(取余)计算,但相比于位运算而言,效率比较低,所以推荐用位运算,而要满足上面这个公式,n 的大小就必须是 2 的 n 次幂。
代码语言:txt复制###### HashMap初始化容量设置问题
要考虑数组长度是2的幂次方、负载因子:只存60个键值对时,2^6 = 64 考虑负载因子 0.75 达到后就会扩容 所以设置 2^7=128
代码语言:txt复制###### hashMap put get过程
代码语言:txt复制###### ConcurrentHashMap Get() 需要加锁吗?
不需要加锁。
保证 put 的时候线程安全之后,get 的时候只需要保证可见性即可,而可见性不需要加锁。
具体是通过Unsafe#getXXXVolatile 和用 volatile 来修饰节点的 val 和 next 指针来实现的。
代码语言:txt复制###### 为什么 ConcurrentHashMap 不支持 key 或者 value 为 null ?
首先, key 为什么也不能为 null ?我不知道,没想明白,可能是作者 lea 佬不喜欢 null 值。
那 value 为什么不能为 null ?
因为在多线程情况下, null 值会产生二义性,因为你不清楚 map 里到底是不存在在这个 key ,还是说被put(key,null)。
代码语言:txt复制### 多线程
###### **创建线程有哪几种方式?**
①. 继承Thread类创建线程类
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。
②. 通过Runnable接口创建线程类
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。
③. 通过Callable和Future创建线程
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
代码语言:txt复制### 锁
#### Java对象头与Monitor对象
在JVM中,对象在内存中存储的布局可以分为三个区域,分别是对象头、实例数据以及填充数据。
实例数据: 存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐。
填充数据: 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
对象头 : 在HotSpot虚拟机中,对象头又被分为两部分,分别为:Mark Word(标记字段)、Class Pointer(类型指针)。如果是数组,那么还会有数组长度。
代码语言:txt复制>### 1.对象头
>
>在对象头的Mark Word中主要存储了对象自身的运行时数据,例如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID以及偏向时间戳等。同时,Mark Word也记录了对象和锁有关的信息。
>
>当对象被synchronized关键字当成同步锁时,和锁相关的一系列操作都与Mark Word有关。由于在JDK1.6版本中对synchronized进行了锁优化,引入了偏向锁和轻量级锁(关于锁优化后边详情讨论)。Mark Word在不同锁状态下存储的内容有所不同。我们以32位JVM中对象头的存储内容如下图所示。
>
>![object_header.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5c081fb4576641eaa63e4966703845ef~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp)
>
>从图中可以清楚的看到,Mark Word中有2bit的数据用来标记锁的状态。无锁状态和偏向锁标记位为01,轻量级锁的状态为00,重量级锁的状态为10。
>
>- 当对象为偏向锁时,Mark Word存储了偏向线程的ID;
>- 当状态为轻量级锁时,Mark Word存储了指向线程栈中Lock Record的指针;
>- 当状态为重量级锁时,Mark Word存储了指向堆中的Monitor对象的指针。
>
>当前我们只讨论重量级锁,因为重量级锁相当于对synchronized优化之前的状态。关于偏向锁和轻量级锁在后边锁优化章节中详细讲解。
>
>可以看到,当为重量级锁时,对象头的MarkWord中存储了指向Monitor对象的指针。那么Monitor又是什么呢?
>### 2.Monitor对象
>
>Monitor对象被称为管程或者监视器锁。在Java中,每一个对象实例都会关联一个Monitor对象。这个Monitor对象既可以与对象一起创建销毁,也可以在线程试图获取对象锁时自动生成。当这个Monitor对象被线程持有后,它便处于锁定状态。
>
>在HotSpot虚拟机中,Monitor是由[ObjectMonitor](https://link.juejin.cn?target=https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/782f3b88b5ba/src/share/vm/runtime/objectMonitor.hpp)实现的,它是一个使用C 实现的类,主要数据结构如下:
>
>```C
>ObjectMonitor() {
> _header = NULL;
> _count = 0; //记录个数
> _waiters = 0,
> _recursions = 0; // 线程重入次数
> _object = NULL;
> _owner = NULL;
> _WaitSet = NULL; // 调用wait方法后的线程会被加入到_WaitSet
> _WaitSetLock = 0 ;
> _Responsible = NULL ;
> _succ = NULL ;
> _cxq = NULL ; // 阻塞队列,线程被唤醒后根据决策判读是放入cxq还是EntryList
> FreeNext = NULL ;
> _EntryList = NULL ; // 没有抢到锁的线程会被放到这个队列
> _SpinFreq = 0 ;
> _SpinClock = 0 ;
> OwnerIsThread = 0 ;
> }
>复制代码
>```
>
>ObjectMonitor中有五个重要部分,分别为_ower,_WaitSet,_cxq,_EntryList和count。
>
>- **_ower** 用来指向持有monitor的线程,它的初始值为NULL,表示当前没有任何线程持有monitor。当一个线程成功持有该锁之后会保存线程的ID标识,等到线程释放锁后_ower又会被重置为NULL;
>- **_WaitSet** 调用了锁对象的wait方法后的线程会被加入到这个队列中;
>- **_cxq** 是一个阻塞队列,线程被唤醒后根据决策判读是放入cxq还是EntryList;
>- **_EntryList** 没有抢到锁的线程会被放到这个队列;
>- **count** 用于记录线程获取锁的次数,成功获取到锁后count会加1,释放锁时count减1。
>
>如果线程获取到对象的monitor后,就会将monitor中的ower设置为该线程的ID,同时monitor中的count进行加1. 如果调用锁对象的wait()方法,线程会释放当前持有的monitor,并将owner变量重置为NULL,且count减1,同时该线程会进入到_WaitSet集合中等待被唤醒。
>
>另外_WaitSet,_cxq与_EntryList都是链表结构的队列,存放的是封装了线程的ObjectWaiter对象。如果不深入虚拟机查看相关源码很难理解这几个队列的作用,关于源码会在后边系列文章中分析。这里我简单说下它们之间的关系,如下:
>
>在多条线程竞争monitor锁的时候,所有没有竞争到锁的线程会被封装成ObjectWaiter并加入_EntryList队列。 当一个已经获取到锁的线程,调用锁对象的wait方法后,线程也会被封装成一个ObjectWaiter并加入到_WaitSet队列中。 当调用锁对象的notify方法后,会根据不同的情况来决定是将_WaitSet集合中的元素转移到_cxq队列还是_EntryList队列。 等到获得锁的线程释放锁后,又会根据条件来执行_EntryList中的线程或者将_cxq转移到_EntryList中再执行_EntryList中的线程。
>
>所以,可以看得出来,_WaitSet存放的是处于WAITING状态等待被唤醒的线程。而_EntryList队列中存放的是等待锁的BLOCKED状态。_cxq队列仅仅是临时存放,最终还是会被转移到_EntryList中等待获取锁。
>
>了解了对象头和Monitor,那么synchronized关键字到底是如何做到与monitor关联的呢?
#### synchronized关键字
synchronized的特性:
1.1 原子性
所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
1.2 可见性
可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。
1.3 有序性
有序性值程序执行的顺序按照代码先后执行。
synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
1.4 可重入性
synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
代码语言:txt复制###### synchronized底层实现原理
1.同步代码块
在字节码中会在同步代码块的入口和出口加上monitorenter和moniterexit指令。当执行到monitorenter指令时,线程就会去尝试获取该对象对应的Monitor的所有权,即尝试获得该对象的锁。
当该对象的 monitor 的计数器count为0时,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有该对象monitor的持有权,那它可以重入这个 monitor ,计数器的值也会加 1。而当执行monitorexit指令时,锁的计数器会减1。
倘若其他线程已经拥有monitor 的所有权,那么当前线程获取锁失败将被阻塞并进入到_EntryList中,直到等待的锁被释放为止。也就是说,当所有相应的monitorexit指令都被执行,计数器的值减为0,执行线程将释放 monitor(锁),其他线程才有机会持有 monitor 。
2.同步方法的实现
同步方法的字节码文件并没有monitorenter和moniterexit两条指令,而是在方法的flag上加入了ACC_SYNCHRONIZED的标记位。这其实也容易理解,因为整个方法都是同步代码,因此就不需要标记同步代码的入口和出口了。当线程线程执行到这个方法时会判断是否有这个ACC_SYNCHRONIZED标志,如果有的话则会尝试获取monitor对象锁。
代码语言:txt复制###### synchronized锁优化
JDK1.6中引入偏向锁和轻量级锁对synchronized进行优化。此时的synchronized一共存在四个状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁着锁竞争激烈程度,锁的状态会出现一个升级的过程。即可以从偏向锁升级到轻量级锁,再升级到重量级锁。锁升级的过程是单向不可逆的,即一旦升级为重量级锁就不会再出现降级的情况。
代码语言:txt复制#### Lock
代码语言:txt复制###### ReentrantLock
代码语言:txt复制#### 乐观锁和悲观锁?
乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。
因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。
因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
代码语言:txt复制###### **乐观锁的实现原理**
乐观锁的实现方式主要有两种:CAS机制和版本号机制
代码语言:txt复制## MySQL
### **数据库的三范式是什么**
第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。
第二范式:要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。
第三范式:任何非主属性不依赖于其它非主属性。
代码语言:txt复制### 索引
###### B 树和 B 树有什么不同呢?
1、B 树每一个节点里存的是数据,而 B 树存储的是索引(地址),使得整个 B 树高度降低,减少了磁盘 IO。所以 B 树里一个节点存不了很多个数据,但是 B 树一个节点能存很多索引,B 树叶子节点存所有的数据。
2、B 树的叶子节点是数据阶段用了一个链表串联起来,便于范围查找。
代码语言:txt复制###### B Tree索引和Hash索引区别?
1、因为Hash索引底层是哈希表,哈希表是一种以Key-value存储数据的结构,所以在存储数据时是没有顺序关系的,Hash索引只适合等值查询,无法进行范围查询
2、Hash索引没办法利用索引完成排序
3、Hash索引也不支持多列联合索引的最左匹配规则
4、如果有大量重复键值的情况下,Hash索引的效率会很低,因为存在哈希碰撞
代码语言:txt复制###### 聚簇索引和非聚簇索引
非聚集索引方式:
代码语言:txt复制MyISAM 在建表时以主键作为 KEY 来建立主索引 B 树,树的叶子节点存的是对应数据的物理地址。我们拿到这个物理地址后,就可以到 MyISAM 数据文件中直接定位到具体的数据记录了。
聚簇索引:
代码语言:txt复制聚簇索引查询会更快,首先 InnoDB 会根据主键 ID 作为 KEY 建立索引 B 树,而 B 树的叶子节点存储的是主键 ID 对应的数据。
代码语言:txt复制比如我们要给 user_name 这个字段加索引,那么 InnoDB 就会建立 user_name 索引 B 树,
代码语言:txt复制节点里存的是 user_name 这个 KEY,
代码语言:txt复制叶子节点存储的数据的是主键 KEY。
代码语言:txt复制拿到主键 KEY 后,InnoDB 再去主键索引树里根据刚在 user_name 索引树找到的主键 KEY 查找到对应的数据。
代码语言:txt复制###### 覆盖索引
1、覆盖索引是当 select 的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖。
在name字段建立了一个索引
EXPLAIN SELECT id,name FROM student
看它的执行计划,为Using index
这是因为索引叶子节点存储了主键id,而name也是索引,所以查询为覆盖索引。
代码语言:txt复制###### 索引优化
代码语言:txt复制### 存储引擎
**数据**和**索引**怎么组织设计,设计理念的不同也导致了 Innodb 和 Myisam 的出现,各自呈现独特的性能。
###### Innodb 引擎和Myisam 引擎的区别
1、MyISAM 虽然数据查找性能极佳,但是不支持事务处理。
2、Innodb 最大的特色就是支持了 ACID 兼容的事务功能,而且他支持行级锁。
3、两个引擎底层数据和索引的组织方式并不一样
代码语言:txt复制Innodb 创建表后生成的文件有:
代码语言:txt复制 frm:创建表的语句
代码语言:txt复制 idb:表里面的数据 索引文件
代码语言:txt复制Myisam 创建表后生成的文件有:
代码语言:txt复制 frm:创建表的语句
代码语言:txt复制 MYD:表里面的数据文件(myisam data)
代码语言:txt复制 MYI:表里面的索引文件(myisam index)
代码语言:txt复制MyISAM 引擎把数据和索引分开了,一人一个文件,这叫做非聚集索引方式;
代码语言:txt复制Innodb 引擎把数据和索引放在同一个文件里了,这叫做聚集索引方式。
代码语言:txt复制###### MyISAM 引擎的底层实现(非聚集索引方式)
MyISAM 在建表时以主键作为 KEY 来建立主索引 B 树,树的叶子节点存的是对应数据的物理地址。
我们拿到这个物理地址后,就可以到 MyISAM 数据文件中直接定位到具体的数据记录了。
代码语言:txt复制###### Innodb 引擎的底层实现(聚集索引方式)
首先 InnoDB 会根据主键 ID 作为 KEY 建立索引 B 树,而 B 树的叶子节点存储的是主键 ID 对应的数据,建表的时候 InnoDB 就会自动建立好主键 ID 索引树,这也是为什么 Mysql 在建表时要求必须指定主键的原因。
比如我们要给 user_name 这个字段加索引,那么 InnoDB 就会建立 user_name 索引 B 树,节点里存的是 user_name 这个 KEY,叶子节点存储的数据的是主键 KEY。
注意,叶子存储的是主键 KEY!拿到主键 KEY 后,InnoDB 才会去主键索引树里根据刚在 user_name 索引树找到的主键 KEY 查找到对应的数据。
代码语言:txt复制### 事务
###### 事务
### 隔离级别
###### ACID
原子性(Atomicity):
一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性
Mysql怎么保证原子性的?
OK,是利用Innodb的undo log。
undo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息。
例如
(1)当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据
(2)当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作
(3)当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操
一致性(Consistency):
数据库总是从一个一致性的状态转换到另一个一致性的状态。(在前面的例子中,一致性确保了,即使在转账过程中系统崩溃,支票账户中也不会损失200美元,因为事务最终没有提交,所以事务中所做的修改也不会保存到数据库中。)
Mysql怎么保证一致性的?
OK,这个问题分为两个层面来说。
从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。
但是,如果你在事务里故意写出违反约束的代码,一致性还是无法保证的。例如,你在转账的例子中,你的代码里故意不给B账户加钱,那一致性还是无法保证。因此,还必须从应用层角度考虑。
从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据!
隔离性(Isolation)
通常来说,一个事务所做的修改操作在提交事务之前,对于其他事务来说是不可见的。(在前面的例子中,当执行完第三条语句、第四条语句还未开始时,此时有另外的一个账户汇总程序开始运行,则其看到支票帐户的余额并没有被减去200美元。)
持久性(Durability)
一旦事务提交,则其所做的修改会永久保存到数据库。
Mysql怎么保证持久性的?
OK,是利用Innodb的redo log。
正如之前说的,Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。
怎么解决这个问题?
简单啊,事务提交前直接把数据写入磁盘就行啊。
这么做有什么问题?
只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16kb大小,你只改其中一点点东西,就要将16kb的内容刷入磁盘,听着也不合理。
毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。显然操作随机IO,速度会比较慢。
于是,决定采用redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
代码语言:txt复制###### 四种隔离级别
READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
默认REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
代码语言:txt复制###### 脏读、不可重复读、幻读
脏读
1、在事务A执行过程中,事务A对数据资源进行了修改,事务B读取了事务A修改后的数据。
2、由于某些原因,事务A并没有完成提交,发生了回滚操作,则事务B读取的数据就是脏数据。
这种读取到另一个事务未提交的数据的现象就是脏读(Dirty Read)。
不可重复读
事务B读取了两次数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的数据不一致。
这种在同一个事务中,前后两次读取的数据不一致的现象就是不可重复读(Nonrepeatable Read)。
幻读
事务B前后两次读取同一个范围的数据,在事务B两次读取的过程中事务A新增了数据,导致事务B后一次读取到前一次查询没有看到的行。
幻读和不可重复读有些类似,但是幻读强调的是集合的增减,而不是单条数据的更新。
代码语言:txt复制### MVCC
MVCC(Multi Version Concurrency Control的简称),代表多版本并发控制。与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。
MVCC最大的优势:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能
读-读:不存在任何问题,也不需要并发控制
读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
代码语言:txt复制###### 当前读和快照读
当前读
像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,
即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
代码语言:txt复制###### 原理
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。所以我们先来看看这个三个point的概念
MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。
每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
代码语言:txt复制###### 总结
总之,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:
MVCC 悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突
MVCC 乐观锁
MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
代码语言:txt复制### 日志
innodb事务日志包括redo log和undo log。redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作。
Bin Log:是mysql服务层产生的日志,常用来进行数据恢复、数据库复制,常见的mysql主从架构,就是采用slave同步master的binlog实现的
Redo Log:记录了数据操作在物理层面的修改,mysql中使用了大量缓存,修改操作时会直接修改内存,而不是立刻修改磁盘,事务进行中时会不断的产生redo log,在事务提交时进行一次flush操作,保存到磁盘中。当数据库或主机失效重启时,会根据redo log进行数据的恢复,如果redo log中有事务提交,则进行事务提交修改数据。
Undo Log: 除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它记录了修改的反向操作,比如,插入对应删除,修改对应修改为原来的数据,通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC
代码语言:txt复制## Redis
###### 缓存穿透、缓存击穿、缓存雪崩
> > 缓存穿透
> >
> > 表示查询一个一定不存在的数据,由于没有获取到缓存,所以没写入缓存,导致这个不存在的数据每次都需要去数据库查询,失去了缓存的意义。
> >
> > 请求的数据大量的没有获取到缓存,导致走数据库,有可能搞垮数据库,使整个服务瘫痪。
> >
> > 解决
> >
> > 1、对于像ID为负数的非法请求直接过滤掉,采用布隆过滤器(Bloom Filter)。
> >
> > 2、针对在数据库中找不到记录的,我们仍然将该空数据存入缓存中,当然一般会设置一个较短的过期时间。
> >
> > 缓存击穿
> >
> > 表示某个key的缓存非常热门,有很高的并发一直在访问,如果该缓存失效,那同时会走数据库,压垮数据库。
> >
> > 缓存击穿与缓存雪崩的区别是这里针对的是某一热门key缓存,而雪崩针对的是大量缓存的集中失效。
> >
> > 解决
> >
> > 1、让该热门key的缓存永不过期。
> >
> > 2、使用互斥锁,通过redis的setnx实现互斥锁。
> >
> > 缓存雪崩
> >
> > 表示在某一时间段,缓存集中失效,导致请求全部走数据库,有可能搞垮数据库,使整个服务瘫痪。
> >
> > 解决
> >
> > 1、针对原因1,可以实现redis的高可用,Redis Cluster 或者 Redis Sentinel(哨兵) 等方案。
> >
> > 2、针对原因2,设置缓存过期时间时加上一个随机值,避免缓存在同一时间过期。
> >
> > 3、使用双缓存策略,设置两个缓存,原始缓存和备用缓存,原始缓存失效时,访问备用缓存,备用缓存失效时间设置长点。
>
> 问题
>
> Redis热点数据如何处理
>
> (1)利用二级缓存
>
> 比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中。
>
> 针对这种热key请求,会直接从jvm中取,而不会走到redis层。
>
> 假设此时有十万个针对同一个key的请求过来,如果没有本地缓存,这十万个请求就直接怼到同一台redis上了。
>
> 现在假设,你的应用层有50台机器,OK,你也有jvm缓存了。这十万个请求平均分散开来,每个机器有2000个请求,会从JVM中取到value值,然后返回数据。避免了十万个请求怼到同一台redis上的情形。
>
> (2)备份热key
>
> 这个方案也很简单。不要让key走到同一台redis上不就行了。我们把这个key,在多个redis上都存一份不就好了。接下来,有热key请求进来的时候,我们就在有备份的redis上随机选取一台,进行访问取值,返回数据。
>
> 怎么发现热key
>
> 方法一:凭借业务经验,进行预估哪些是热key
>
> 其实这个方法还是挺有可行性的。比如某商品在做秒杀,那这个商品的key就可以判断出是热key。缺点很明显,并非所有业务都能预估出哪些key是热key。
>
> 方法二:在客户端进行收集
>
> 这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。缺点就是对客户端代码造成入侵。
>
> 方法三:在Proxy层做收集
>
> 有些集群架构是下面这样的,Proxy可以是Twemproxy,是统一的入口。可以在Proxy层做收集上报,但是缺点很明显,并非所有的redis集群架构都有proxy。
>
> 方法四:用redis自带命令
>
> (1)monitor命令,该命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key是啥。当然,也有现成的分析工具可以给你使用,比如redis-faina。但是该命令在高并发的条件下,有内存增暴增的隐患,还会降低redis的性能。
>
> (2)hotkeys参数,redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可。但是该参数在执行的时候,如果key比较多,执行起来比较慢。
>
> 方法五:自己抓包评估
>
> Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。自己写程序监听端口,按照RESP协议规则解析数据,进行分析。缺点就是开发成本高,维护困难,有丢包可能性。
###### 数据类型
String字符串类型
场景
(1)缓存结构体信息:
(2)计数功能:
List列表类型
场景
(1)list列表结构常用来做异步队列使用
(2)list可用于秒杀抢购场景
数据结构
ziplist
当存储的数据量较少的时,hash 采用 ziplist 作为底层存储结构,此时要求符合以下两个条件:
哈希对象保存的所有键值对(键和值)的字符串长度总和小于 64 个字节。
哈希对象保存的键值对数量要小于 512 个。
dict(字典结构)
该结构类似于 Java 的 HashMap,是一个无序的字典,并采用了数组和链表相结合的方式存储数据。在 Redis 中,dict 是基于哈希表算法实现的,因此其查找性能非常高效,其时间复杂度为 O(1)。
Hash数据类型
场景
(1)保存结构体信息
Set集合类型
场景
就是用在一些去重的场景里,例如每个用户只能参与一次活动、一个用户只能中奖一次等等去重场景。
Zset有序集合
场景
(1)各类热门排序场景
数据结构
跳表
时间复杂度
有n个结点的链表,假设每两个链表构建一个索引,那么:
第一级索引个数为:n/2;
第二级索引个数为:n/4;
···
第h级索引个数为:n/2^h;
以此类推,在每一层需要通过3个节点找目标数,那么总的时间复杂度就为O(3*logn),因为3是常数,所以最后的时间复杂度为O(logn)。
空间复杂度
链表上的索引数目按第一层,第二层,···,倒数第二层,最后一层的顺序排列下来分别为:n/2,n/4,···,4,2
所以空间复杂度为O(n)
代码语言:txt复制高并发
Redis通过主从架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。
Redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如redis主就10G的内存量,其实你就最对只能容纳10g的数据量。如果你的缓存要容纳的数据量很大,达到了几十g,甚至几百g,或者是几t,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒几十万的读写并发。
Redis主从架构数据会丢失
有两种数据丢失的情况:
异步复制导致的数据丢失:因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。
脑裂导致的数据丢失:某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。
这个时候,集群里就会有两个master,也就是所谓的脑裂。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了。因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。
Redis主从复制的工作原理?
1.一个Slave实例,无论是第一次连接还是重连到Master,它都会发出一个SYNC命令;
2.当Master收到SYNC命令之后,会做两件事:(a) Master执行BGSAVE,即在后台保存数据到磁盘(rdb快照文件);(b) Master同时将新收到的写入和修改数据集的命令存入缓冲区(非查询类);
3.当Master在后台把数据保存到快照文件完成之后,Master会把这个快照文件传送给Slave,而Slave则把内存清空后,加载该文件到内存中;
4.而Master也会把此前收集到缓冲区中的命令,通过Reids命令协议形式转发给Slave,Slave执行这些命令,实现和Master的同步;
5.Master/Slave此后会不断通过异步方式进行命令的同步,达到最终数据的同步一致;
代码语言:txt复制###### 数据一致性问题
合理设置缓存的过期时间。
新增、更改、删除数据库操作时同步更新 Redis,可以使用事物机制来保证数据的一致性。
1.第一种方案:采用延时双删策略
解决方法一:数据实时更新
当更新数据库的时候,同步更新缓存。
优点:数据一致性强,不会出现缓存雪崩的问题。
缺点:代码耦合度高,影响正常业务,增加一次网络开销。
适用环境:适用于数据一致性要求高的场景,比如银行业务,证券交易等
解决方法二:数据准实时更新
当更新数据库的同时,异步去更新缓存,比如更新数据库后把一条消息发送到mq中去实现。
优点:与业务解耦,不影响正常业务,不会出现缓存雪崩。
缺点:有较短的延迟,并且无法保证最终的一致性,需要补偿机制。
适用环境:写操作不频繁并且实时性要求不严格的场景。
解决方法三:缓存失效机制
基于缓存本身的失效机制,具体实现方式为设置缓存失效时间,如果有缓存就从缓存中取数据,如果没缓存就从数据库中取数据,并且重新设置缓存。
优点:实现方式简单,与业务完美解耦,不影响正常业务。
缺点:有一定延迟,并且存在缓存雪崩的情况。
适用环境:适合读多写少的互联网环境,能接受一定的数据延时。
解决方法四:定时任务更新
通过定时任务,按照一定时间间隔更新缓存。
优点:不影响正常业务,在特殊场景应用广泛。
缺点:不保证实时一致性,且需要为每个任务写一个调度代码。
适用环境:适用于需要复杂数据统计的缓存更新,比如展示高速车流量,五分钟一次的统计不会影响业务使用。
代码语言:txt复制###### Redis的事务
相关命令
1. MULTI
用于标记事务块的开始。Redis会将后续的命令逐个放入队列中,然后才能使用EXEC命令原子化地执行这个命令序列。
这个命令的运行格式如下所示:
MULTI
这个命令的返回值是一个简单的字符串,总是OK。
2. EXEC
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
当使用WATCH命令时,只有当受监控的键没有被修改时,EXEC命令才会执行事务中的命令,这种方式利用了检查再设置(CAS)的机制。
这个命令的运行格式如下所示:
EXEC
这个命令的返回值是一个数组,其中的每个元素分别是原子化事务中的每个命令的返回值。 当使用WATCH命令时,如果事务执行中止,那么EXEC命令就会返回一个Null值。
3. DISCARD
清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
如果使用了WATCH命令,那么DISCARD命令就会将当前连接监控的所有键取消监控。
这个命令的运行格式如下所示:
DISCARD
这个命令的返回值是一个简单的字符串,总是OK。
4. WATCH
当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的。
这个命令的运行格式如下所示:
WATCH key key ...
这个命令的返回值是一个简单的字符串,总是OK。
对于每个键来说,时间复杂度总是O(1)。
5. UNWATCH
清除所有先前为一个事务监控的键。
如果你调用了EXEC或DISCARD命令,那么就不需要手动调用UNWATCH命令。
这个命令的运行格式如下所示:
UNWATCH
这个命令的返回值是一个简单的字符串,总是OK。
时间复杂度总是O(1)。
Redis事务允许在一次单独的步骤中执行一组命令,并且可以保证如下两个重要事项:
>Redis会将一个事务中的所有命令序列化,然后按顺序执行。Redis不可能在一个Redis事务的执行过程中插入执行另一个客户端发出的请求。这样便能保证Redis将这些命令作为一个单独的隔离操作执行。
> 在一个Redis事务中,Redis要么执行其中的所有命令,要么什么都不执行。因此,Redis事务能够保证原子性。EXEC命令会触发执行事务中的所有命令。因此,当某个客户端正在执行一次事务时,如果它在调用MULTI命令之前就从Redis服务端断开连接,那么就不会执行事务中的任何操作;相反,如果它在调用EXEC命令之后才从Redis服务端断开连接,那么就会执行事务中的所有操作。
问题
Redis事务保证原子性吗,支持回滚吗
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
代码语言:txt复制###### 持久化机制
redis提供两种方式进行持久化
一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化)
另外一种是AOF持久化(append only file原理是将Reids的操作日志以追加的方式写入文件)。
区别
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
优缺点
RDB
优点
1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
缺点
1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF
优点
1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
缺点
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。
常用配置
RDB持久化配置
Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
AOF持久化配置
在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
代码语言:txt复制###### 优化
缩短键值对的存储长度;
使用 lazy free(延迟删除)特性;
设置键值的过期时间;
禁用长耗时的查询命令;
使用 slowlog 优化耗时命令;
使用 Pipeline 批量操作数据;
避免大量数据同时失效;
客户端使用优化;
限制 Redis 内存大小;
使用物理机而非虚拟机安装 Redis 服务;
检查数据持久化策略;
禁用 THP 特性;
使用分布式架构来增加读写速度。
代码语言:txt复制###### 分布式锁
分布式锁应该满足哪些特性?
互斥:在任何给定时刻,只有一个客户端可以持有锁;
无死锁:任何时刻都有可能获得锁,即使获取锁的客户端崩溃;
容错:只要大多数 Redis的节点都已经启动,客户端就可以获取和释放锁。
可以使用 SETNX key value 命令是实现「互斥」特性。
这个命令来自于SET if Not eXists的缩写,意思是:如果 key 不存在,则设置 value 给这个key,否则啥都不做。Redis 官方地址说的:
1:设置成功;
0:key 没有设置成功。
使用 DEL 删除这个 key 就行。
释放锁
客户端所在节点崩溃,无法正确释放锁;
业务逻辑异常,无法执行 DEL指令。
超时设置
代码语言:txt复制###### Redis超卖问题
答:redis存商品id,每次都只允许一个线程访问。
## Mybatis-puls
###### **mybatis 有几种分页方式?**
数组分页
sql分页
拦截器分页
RowBounds分页
代码语言:txt复制###### **mybatis 逻辑分页和物理分页的区别是什么?**
代码语言:txt复制###### **说一下 mybatis 的一级缓存和二级缓存?**
一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置<cache/> ;
对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
代码语言:txt复制## 消息队列
## Spring
### Spring Boot
###### Spring的核心特性
Spring的核心特性就是IOC和AOP
IOC(Inversion of Control):“控制反转”,即对象创建的问题。通俗地讲就是把创建对象的代码交给了Spring的配置文件来进行的。这样做的优点是大大降低的代码的耦合度。
AOP(Aspect-OrientedProgramming),即“面向切面编程”。
代码语言:txt复制###### Spring boot 相对 Spring有什么优势
传统Spring框架存在的弊端:
Spring事物管理,MVC,启用第三方库都需要XML或Java进行显示配置,配置过重
写配置挤占了实际写应用的逻辑的时间
项目依赖管理,要考虑用那些库,还要知道哪些版本和库不会有冲突,影响开发效率
SpringBoot的优势:
自动配置:针对很多Spring常见的应用功能,SpringBoot能自动提供相关配置
起步依赖:告诉SpringBoot需要什么功能,它就能引入需要的库
CLI命令行界面:通过SpringBootCLI,借此你只需写代码就能完成完整的应用程序,无须传统项目构建
Actuator: 提供在运行时检视应用程序内部情况的能力
代码语言:txt复制### 如何使用Spring IOC
IOC创建实例对象有**两种**方法,一种是配置文件创建,另一种是注解的方法创建。
1、配置文件创建
这里我们需要三步:
第一步、创建配置文件,我们在根目录下创applicationContext.xml(PS:名称可以不唯一)的配置文件,并且主体部分如下
第二步、创建类。(我们之前创建了User类,这里就不演示了)
第三步、配置xml文件
```xml
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
id:用于SpringIOC调用,可以为任意
class:类的全路径
scope:作用范围,scope不是必填属性,不写的默认值单例。为创建对象的方式,有两种结果:
1.singleton默认值,单实例。(常用)
2.prototype 多实例。(常用)
3.request:作用于 web 应用的请求范围
4.session:作用于 web 应用的会话范围
5.global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,就是session
-->
<bean id="user" class="com.testWeb.daomain.User" scope="prototype"></bean>
</beans>
第四步、写测试代码测试
```Java
public class testSpring {
@Test
public void testIOC(){
ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
User user1= (User) context.getBean("user");
User user2=(User) context.getBean("user");
System.out.println(user1 user2);//如果scope为单例,两个对象的地址相同,多例效果则相反
}
}
代码语言:txt复制 **2、注解创建**
首先来指出四种创建对象的四种注解方式:(四种注解本质上创建对象的方法是相同的,名称只是起到了清晰用途的作用)
首先是@Component注解创建,它又衍生出了三种注解:
1. @Controller :web层
2. @Service :service层
3. @repository:持久层
### 如何使用Spring AOP
代码语言:txt复制### Spring初始化Bean
![img](https://img-blog.csdnimg.cn/20200524170259888.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMyNzgwNzQx,size_16,color_FFFFFF,t_70)
Spring单例对象的初始化其实可以分为三步:
1、createBeanInstance, 实例化,实际上就是调用对应的构造方法构造对象,此时只是调用了构造方法,spring xml中指定的property并没有进行populate
2、populateBean,填充属性,这步对spring xml中指定的property进行populate
3、initializeBean,调用spring xml中指定的init方法,或者AfterPropertiesSet方法。
代码语言:txt复制###### Bean 的生命周期
如上图所示,Bean 的生命周期还是比较复杂的,下面来对上图每一个步骤做文字描述:
Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
Bean实例化后对将Bean的引入和值注入到Bean的属性中
如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
如果bean有被@PostConstruct注解的方法,会执行该方法;如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
如果bean有被@PreDestroy注解的方法,执行该方法;如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。
代码语言:txt复制###### Bean的作用域
![img](https://img-blog.csdnimg.cn/20200524172104925.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMyNzgwNzQx,size_16,color_FFFFFF,t_70)
###### spring怎么解决bean的循环依赖
spring中循环依赖有三种情况:
1、构造器注入形成的循环依赖。也就是beanB需要在beanA的构造函数中完成初始化,beanA也需要在beanB的构造函数中完成舒适化,这种情况的结果就是两个bean都不能完成初始化,循环依赖难以解决。
2、setter注入构成的循环依赖。beanA需要在beanB的setter方法中完成初始化,beanB也需要在beanA的setter方法中完成初始化,spring设计的机制主要就是解决这种循环依赖。
3、prototype作用域bean的循环依赖。这种循环依赖同样无法解决,因为spring不会缓存‘prototype’作用域的bean,而spring中循环依赖的解决正是通过缓存来实现的。
4.解决方法
4.1 重新设计
4.2 使用注解 @Lazy
4.3 使用Setter/Field注入
4.4 使用@PostConstruct
4.5 实现ApplicationContextAware与InitializingBean
代码语言:txt复制###### **setter注入构成的循环依赖解决方案**
步骤一:beanA进行初始化,并且将自己进行初始化的状态记录下来,并提前向外暴露一个单例工程方法,从而使其他bean能引用到该bean(可能读完这一句,您仍然心存疑惑,没关系,继续往下读)
步骤二:beanA中有beanB的依赖,于是开始初始化beanB。
步骤三:初始化beanB的过程中又发现beanB依赖了beanA,于是又进行beanA的初始化,这时发现beanA已经在进行初始化了,程序发现了存在的循环依赖,然后通过步骤一中暴露的单例工程方法拿到beanA的引用(注意,此时的beanA只是完成了构造函数的注入但为完成其他步骤),从而beanB拿到beanA的引用,完成注入,完成了初始化,如此beanB的引用也就可以被beanA拿到,从而beanA也就完成了初始化。
spring进行bean的加载的时候,首先进行bean的初始化(调用构造函数),然后进行属性填充。在这两步中间,spring对bean进行了一次状态的记录,也就是说spring会把指向只完成了构造函数初始化的bean的引用通过一个变量记录下来,明白这一点对之后的源码理解至关重要。
代码语言:txt复制### Spring中实现事务方式
Spring 提供了两种方式实现事务:①声明式,②编程式。
Spring 并不直接支持事务,只有当数据库支持事务时,Spring 才支持事务,Spring 只不过简化了开发人员实现事务的步骤。
代码语言:txt复制**编程式事务:**允许用户在代码中精确定义事务的边界。
**声明式事务:**(基于AOP)有助于用户将操作与事务规则进行解耦。简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。
> **声明式事务**
声明式事务管理只需要用到@Transactional 注解和@EnableTransactionManagement(开启事务管理功能)。它是基于 **Spring AOP** 实现的,并且通过注解实现,实现起来简单,对原有代码没有入侵性。
###### **事务的7种传播级别**
1) PROPAGATION_REQUIRED ,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
2)PROPAGATION_SUPPORTS ,从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
3)PROPAGATION_MANDATORY , 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
4)PROPAGATION_REQUIRES_NEW ,从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。
5)PROPAGATION_NOT_SUPPORTED ,这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了。用当前级别的事务模板抱起来就可以了。
6)PROPAGATION_NEVER ,该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。
7)PROPAGATION_NESTED ,字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
代码语言:txt复制###### Autowire是按照什么规则来获取Bean
当Spring装配Bean属性时,有时候非常明确,就是需要将某个Bean的引用装配给指定属性。
比如,如果我们的应用上下文中只有一个org.mybatis.spring.SqlSessionFactoryBean 类型的Bean
那么任意一个依赖SqlSessionFactoryBean的其他Bean就是需要这个Bean。毕竟这里只有一个SqlSessionFactoryBean的Bean。
为了应对这种明确的装配场景,Spring提供了自动装配(autowiring)。与其显式的装配Bean属性,为何不让Spring识别出可以自动装配的场景。
当涉及到自动装配Bean的依赖关系时,Spring有多种处理方式。因此,Spring提供了4种自动装配策略。
代码语言:txt复制//无需自动装配
代码语言:txt复制int AUTOWIRE_NO = 0;
代码语言:txt复制//按名称自动装配bean属性
代码语言:txt复制int AUTOWIRE_BY_NAME = 1;
代码语言:txt复制//按类型自动装配bean属性
代码语言:txt复制int AUTOWIRE_BY_TYPE = 2;
代码语言:txt复制//按构造器自动装配
代码语言:txt复制int AUTOWIRE_CONSTRUCTOR = 3;
代码语言:txt复制###### 除了Autowire其他获取Bean的方法
一:在初始化时保存ApplicationContext对象
可以在初始化的时候保存ApplicationContext对象,然后通过这个对象获取Bean,测试代码如下:
/**
* 方式二:使用ClassPathXmlApplicationContext获取ApplicationContext
*/
@Test
public void getBeanTest2() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserInfo userInfo = (UserInfo) applicationContext.getBean("userInfo");
System.out.println(userInfo);
}
:使用BeanFactory直接获取(不推荐)
使用BeanFactory从工厂中直接获取Bean实例,但是XmlBeanFactory类已经废弃,因此不建议使用,测试代码如下:
/**
* 方式一:XmlBeanFactory已经废弃不建议使用
*/
@Test
public void getBeanTest1() {
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
UserInfo userInfo = (UserInfo) beanFactory.getBean("userInfo");
System.out.println(userInfo);
}
三:继承自抽象类ApplicationObjectSupport
可以继承抽象类ApplicationObjectSupport并将自己继承的类注入到Spring容器中,示例代码如下:
/**
* 方法三:继承ApplicationObjectSupport来获取ApplicationContext,
* 注意:需要把自己继承的类注入到Spring
*/
@Test
public void getBeanTest3() {
ApplicationContextUtil2 applicationContextUtil2 = (ApplicationContextUtil2) ApplicationContextUtil.getBean("applicationContextUtil2");
UserInfo userInfo = (UserInfo) applicationContextUtil2.getBean("userInfo");
System.out.println(userInfo);
}
四:继承自抽象类WebApplicationObjectSupport
可以继承抽象类WebApplicationObjectSupport并将自己继承的类注入到Spring容器中,示例代码如下:
/**
* 方法四:继承WebApplicationObjectSupport来获取ApplicationContext,
* 注意:需要把自己继承的类注入到Spring,同时需要添加@WebAppConfiguration注解,否则会找不到web容器
*/
@Test
public void getBeanTest4() {
ApplicationContextUtil3 applicationContextUtil3 = (ApplicationContextUtil3) ApplicationContextUtil.getBean("applicationContextUtil3");
UserInfo userInfo = (UserInfo) applicationContextUtil3.getBean("userInfo");
System.out.println(userInfo);
}
## Dubbo
###### dubbo核心概念
Apache Dubbo 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。