图灵课堂:https://www.bilibili.com/video/BV1Vq4y187H8/?spm_id_from=333.788.recommend_more_video.5

一、Java基础(33)

1、面向对象

什么是面向对象?对比面向过程,是两种不同的处理问题的⻆度,面向过程更注重事情的每⼀个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要什么,比如洗衣机洗衣服:

  1. 面向过程:会将任务拆解成⼀系列的步骤:打开洗衣服——->放衣服——->放洗衣粉——->清洗——->烘干
  2. 面向对象:会拆出人和洗衣机两个对象:
    • 人:打开洗衣机 放衣服 放洗衣粉
    • 洗衣粉:清洗 烘干

从以上例子能看出,面向过程比较直接高校,而面向对象更易于复用、扩展和维护。
1.1 封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透 明,外部调用无需修改或者关心内部实现
image.png
1.2 继承:继承基类的方法,并做出自己的改变和或扩展,
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的
1.3 多态: 基于对象所属类的不同,外部对同⼀个方法的调用,实际执行的逻辑不同。
1.3.1 多态的形式
父类的引用指向子类的对象
多态是出现在继承或者实现关系中的。
多态体现的格式:

  1. 父类类型 变量名 = new 子类/实现类构造器;
  2. 变量名.方法名();

多态的前提:有继承关系,子类对象是可以赋值给父类类型的变量。例如Animal是一个动物类型,而Cat是一个猫类型。Cat继承了Animal,Cat对象也是Animal类型,自然可以赋值给父类类型的变量。
1.3.2 多态的案例演示
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。如果子类没有重写该方法,就会调用父类的该方法。
总结起来就是:编译看左边,运行看右边。

  1. public class Animal {
  2. public void eat(){
  3. System.out.println("动物吃东西!");
  4. }
  1. class Cat extends Animal {
  2. public void eat() {
  3. System.out.println("吃鱼");
  4. }
  5. }
  6. class Dog extends Animal {
  7. public void eat() {
  8. System.out.println("吃骨头");
  9. }
  10. }
  1. public class Test {
  2. public static void main(String[] args) {
  3. // 多态形式,创建对象
  4. Animal a1 = new Cat();
  5. // 调用的是 Cat 的 eat
  6. a1.eat();
  7. // 多态形式,创建对象
  8. Animal a2 = new Dog();
  9. // 调用的是 Dog 的 eat
  10. a2.eat();
  11. }
  12. }

1.3.3 多态的定义和前提
多态:是指同一行为,具有多个不同表现形式。
从上面案例可以看出,Cat和Dog都是动物,都是吃这一行为,但是出现的效果(表现形式)是不一样的。
前提【重点】
继承或者实现【二选一】方法的重写【意义体现:不重写,无意义】父类引用指向子类对象【格式体现】父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
1.3.4 多态的好处
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。代码如下:

  1. public abstract class Animal {
  2. public abstract void eat();
  3. }
  1. class Cat extends Animal {
  2. public void eat() {
  3. System.out.println("吃鱼");
  4. }
  5. }
  6. class Dog extends Animal {
  7. public void eat() {
  8. System.out.println("吃骨头");
  9. }
  10. }
  1. public class Test {
  2. public static void main(String[] args) {
  3. // 多态形式,创建对象
  4. Cat c = new Cat();
  5. Dog d = new Dog();
  6. // 调用showCatEat
  7. showCatEat(c);
  8. // 调用showDogEat
  9. showDogEat(d);
  10. /* 以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代 而执行效果一致 */
  11. showAnimalEat(c);
  12. showAnimalEat(d);
  13. }
  14. public static void showCatEat (Cat c){
  15. c.eat();
  16. }
  17. public static void showDogEat (Dog d){
  18. d.eat();
  19. }
  20. public static void showAnimalEat (Animal a){
  21. a.eat();
  22. }
  23. }

由于多态特性的支持,showAnimalEat方法的Animal类型,是Cat和Dog的父类类型,父类类型接收子类对象,当然可以把Cat对象和Dog对象,传递给方法。
当eat方法执行时,多态规定,执行的是子类重写的方法,那么效果自然与showCatEat、showDogEat方法一致,所以showAnimalEat完全可以替代以上两方法。
不仅仅是替代,在扩展性方面,无论之后再多的子类出现,我们都不需要编写showXxxEat方法了,直接使用showAnimalEat都可以完成。从而实现了实现类的自动切换。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
1.3.5 多态的弊端
我们已经知道多态编译阶段是看左边父类类型的,如果子类有些独有的功能,此时多态的写法就无法访问子类独有功能了。

  1. class Animal{
  2. public void eat(){
  3. System.out.println("动物吃东西!");
  4. }
  5. class Cat extends Animal {
  6. public void eat() {
  7. System.out.println("吃鱼");
  8. }
  9. public void catchMouse() {
  10. System.out.println("抓老鼠");
  11. }
  12. }
  13. class Dog extends Animal {
  14. public void eat() {
  15. System.out.println("吃骨头");
  16. }
  17. }
  18. class Test{
  19. public static void main(String[] args){
  20. Animal a = new Cat();
  21. a.eat(); a.catchMouse();//编译报错,编译看左边,Animal没有这个方法
  22. }
  23. }

1.3.6 引用类型转换
1、为什么要转型
多态的写法就无法访问子类独有功能了。
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点”小麻烦”。所以,想要调用子类特有的方法,必须做向下转型。
回顾基本数据类型转换
自动转换:范围小的赋值给范围大的,自动完成:double d = 5;
强制转换:范围大的赋值给范围小的,强制转换:int i = (int)3.14
多态的转型分为向上转型(自动转换)与向下转型(强制转换)两种。
2、向上转型(自动转换)
向上转型:多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。
使用格式:父类类型 变量名 = new 子类类型();
如:Animal a = newCat();
原因是:父类类型相对与子类来说是大范围的类型,Animal是动物类,是父类类型。Cat是猫类,是子类类型。Animal类型的范围当然很大,包含一切动物。所以子类范围小可以直接自动转型给父类类型的变量。
3、向下转型(强制转换)
向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。 一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
使用格式:子类类型 变量名 = (子类类型) 父类变量名;
如:Aniaml a = newCat(); Cat c = (Cat) a;
4、案例演示
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点”小麻烦”。所以,想要调用子类特有的方法,必须做向下转型。

  1. abstract class Animal {
  2. abstract void eat();
  3. }
  4. class Cat extends Animal {
  5. public void eat() {
  6. System.out.println("吃鱼");
  7. }
  8. public void catchMouse() {
  9. System.out.println("抓老鼠");
  10. }
  11. }
  12. class Dog extends Animal {
  13. public void eat() {
  14. System.out.println("吃骨头");
  15. }
  16. public void watchHouse() {
  17. System.out.println("看家");
  18. }
  19. }
  1. public class Test {
  2. public static void main(String[] args) {
  3. // 向上转型
  4. Animal a = new Cat();
  5. a.eat();
  6. // 调用的是 Cat 的 eat
  7. // 向下转型
  8. Cat c = (Cat)a; c.catchMouse();
  9. // 调用的是 Cat 的 catchMouse
  10. }
  11. }

5、转型的异常
转型的过程中,一不小心就会遇到这样的问题,请看如下代码:

  1. public class Test {
  2. public static void main(String[] args) {
  3. // 向上转型
  4. Animal a = new Cat();
  5. a.eat();
  6. // 调用的是 Cat 的 eat
  7. // 向下转型
  8. Dog d = (Dog)a;
  9. d.watchHouse();
  10. // 调用的是 Dog 的 watchHouse 【运行报错】
  11. }
  12. }

这段代码可以通过编译,但是运行时,却报出了 ClassCastException ,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。
6、instanceof关键字
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,格式如下:
变量名 instanceof 数据类型如果变量属于该数据类型或者其子类类型,返回true。如果变量不属于该数据类型或者其子类类型,返回false。
所以,转换前,我们最好先做一个判断,代码如下:

  1. public class Test {
  2. public static void main(String[] args) {
  3. // 向上转型
  4. Animal a = new Cat();
  5. a.eat(); // 调用的是 Cat 的 eat
  6. // 向下转型
  7. if (a instanceof Cat){
  8. Cat c = (Cat)a;
  9. c.catchMouse(); // 调用的是 Cat 的 catchMouse
  10. } else if (a instanceof Dog){
  11. Dog d = (Dog)a;
  12. d.watchHouse(); // 调用的是 Dog 的 watchHouse
  13. }
  14. }
  15. }

2、JDK、JRE、JVM之间的区别

JDK:Java Develpment Kit java 开发工具
JRE:Java Runtime Environment java运行时环境
JVM:java Virtual Machine java 虚拟机
image.png

3、==和equals方法之间的区别

==:对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
equals:object中默认也是采用 ==比较,通常会重写

  1. public boolean equals(Object obj) {
  2. return (this == obj);
  3. }
  1. public boolean equals(Object anObject) {
  2. if (this == anObject) {
  3. return true;
  4. }
  5. if (anObject instanceof String) {
  6. String anotherString = (String)anObject;
  7. int n = value.length;
  8. if (n == anotherString.value.length) {
  9. char v1[] = value;
  10. char v2[] = anotherString.value;
  11. int i = 0;
  12. while (n-- != 0) {
  13. if (v1[i] != v2[i])
  14. return false;
  15. i++;
  16. }
  17. return true;
  18. }
  19. }
  20. return false;
  21. }

上述代码可以看出,String类中被复写的equals()方法其实是比较两个字符串的内容。

  1. public class StringDemo {
  2. public static void main(String args[]) {
  3. String str1 = "Hello";
  4. String str2 = new String("Hello");
  5. String str3 = str2; // 引⽤传递
  6. System.out.println(str1 == str2); // false
  7. System.out.println(str1 == str3); // false
  8. System.out.println(str2 == str3); // true
  9. System.out.println(str1.equals(str2)); // true
  10. System.out.println(str1.equals(str3)); // true
  11. System.out.println(str2.equals(str3)); // true
  12. }
  13. }

4、hashCode()与equals()之间的关系

HashCode介绍:hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回⼀个int整数 。这 个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,Java中的任何类都包含有hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用 到了散列码!( 可以快速找到所需要的对象)

以“HashSet如何检查重复”为例子来说明为什么要有hashCode
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、HashSet会假设对象没有重复出现 。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重 新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。

  1. 如果两个对象相等,则hashcode⼀定也是相同的
  2. 两个对象相等,对两个对象分别调用equals方法都返回true
  3. 两个对象有相同的hashcode值,它们也不⼀定是相等的
  4. 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值 。如果没有重写hashCode(),则该class的两个 对象无论如何都不会相等 ( 即使这两个对象指向相同的数据)

    5、final关键字的作用是什么?

    修饰类:表示类不可被继承
    修饰方法:表示方法不可被子类覆盖,但是可以重载
    修饰变量:表示变量⼀旦被赋值就不可以更改它的值。
    修饰成员变量:

  6. 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。

  7. 如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。

修饰局部变量:
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值 ( 仅⼀次)

  1. public class FinalVar {
  2. final static int a = 0; //再声明的时候就需要赋值 或者静态代码块赋值
  3. /**
  4. static{ a = 0; }
  5. */
  6. final int b = 0; //再声明的时候就需要赋值 或者代码块中赋值 或者构造器赋值
  7. /*{
  8. b = 0;
  9. }*/
  10. public static void main(String[] args) {
  11. final int localA; //局部变量只声明没有初始化,不会报错,与final⽆关。
  12. localA = 0;//在使⽤之前⼀定要赋值
  13. //localA = 1; 但是不允许第⼆次赋值
  14. }
  15. }

修饰基本类型数据和引用类型数据:

  1. 如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;
  2. 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。但是引用的值是可变的

    1. public class FinalReferenceTest{
    2. public static void main(){
    3. final int[] iArr={1,2,3,4};
    4. iArr[2]=-3; //合法
    5. iArr=null; //⾮法,对iArr不能重新赋值
    6. final Person p = new Person(25);
    7. p.setAge(24); //合法
    8. p=null; //⾮法
    9. }
    10. }

    6、为什么局部内部类和匿名内部类只能访问局部final变量?

    编译之后会生成两个class文件,Test.class Test1.class ```java public class Test { public static void main(String[] args) {

    } //局部final变量a,b public void test(final int b) {//jdk8在这⾥做了优化, 不⽤写,语法糖,但实际上也是 有的,也不能修改

    1. final int a = 10;
    2. //匿名内部类
    3. new Thread(){
    4. public void run() {
    5. System.out.println(a);
    6. System.out.println(b);
    7. };
    8. }.start();

    } }

class OutClass { private int age = 12; public void outPrint(final int x) { class InClass { public void InPrint() { System.out.println(x); System.out.println(age); } } new InClass().InPrint(); } }

  1. 首先需要知道的⼀点是: 内部类和外部类是处于同⼀个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
  2. 这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡) 。这里就出现了⼀个矛盾:内部类对象访问了⼀个不存在的变量。为了解决这个问题,就将局部变量复制了⼀份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy" 。这样就好像延长了局部变量的生命周期
  3. 将局部变量复制为内部类的成员变量时,必须保证这两个变量是⼀样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
  4. 就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的⼀致性。这实际上也是⼀种妥协。使得局部变量与内部类内建立的拷贝保持⼀致。
  5. <a name="gy1zf"></a>
  6. ## 7、String、StringBuffer、StringBuilder的区别
  7. String是不可变的,如果尝试去修改,会新生成⼀个字符串对象,StringBufferStringBuilder是可变的<br />StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高
  8. <a name="EMq5b"></a>
  9. ## 8、重载和重写的区别
  10. 重载:发生在同⼀个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。<br />重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
  11. ```java
  12. public int add(int a,String b)
  13. public String add(int a,String b) //编译报错

9、接口和抽象类的区别

  1. 抽象类可以存在普通成员函数,而接口中只能存在public abstract 方法。
  2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
  3. 抽象类只能继承⼀个,接口可以实现多个。

接口的设计目的,是对类的行为进行约束 ( 更准确的说是⼀种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供⼀种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。

而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中⼀部分行为的实现方式⼀致时(A的非真子集,记为B),可以让这些类都派生于⼀个抽象类 。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自已实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
抽象类是对类本质的抽象,表达的是is a 的关系,比如:BMW is a Car 。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是 like a 的关系 。比如:Bird like a Aircraft ( 像飞行器⼀样可以飞) ,但其本质上 is a Bird 。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。

使用场景:当你关注⼀个事物的本质的时候,用抽象类;当你关注⼀个操作的时候,用接口。

抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语⾔来说 ( 从实际设计上来说也 是) 每个类只能继承⼀个类 。在这个类中,你必须继承或编写出其所有子类的所有共性 。虽然接口在功能上会弱化许多,但是它只是针对⼀个动作的描述。而且你可以在⼀个类中同时实现多个接口。在设计阶段会降低难度

10、List和Set的区别

List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,在逐⼀遍历,还可以使用get(int index)获取指定下标的元素
Set:无序,不可重复,最多允许有⼀个Null元素对象,取元素时只能用Iterator接口取得所有元素,在逐⼀遍历各个元素

11、ArrayList和LinkedList区别

  1. 首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
  2. 由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
  3. 另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用

    12、HashMap和HashTable有什么区别?其底层实现是什么?

    区别:

  4. HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;

  5. HashMap允许key和value为null,而HashTable不允许

底层实现:数组+链表实现 ,jdk8开始链表高度到8 、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在

  1. 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,
  2. 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,
  3. 如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
  4. key为null,存在下标0的位置

    13、谈谈ConcurrentHashMap的扩容机制

    1.7 版本:

    1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
    2. 每个Segment相对于⼀个小型的HashMap
    3. 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
    4. 先生成新的数组,然后转移元素到新数组中
    5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本:

  1. 1.8版本的ConcurrentHashMap不再基于Segment实现
  2. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程⼀起进行扩容
  3. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然 后判断是否超过阈值,超过了则进行扩容
  4. ConcurrentHashMap是支持多个线程同时扩容的
  5. 扩容之前也先生成⼀个新的数组
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责⼀组或 多组的元素转移工作

    14、Jdk1.7到Jdk1.8 HashMap 发生了什么变化(底层)?

  1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
  2. 1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
  3. 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源

    15、说⼀下HashMap的Put方法

    先说HashMap的Put方法的大体流程:

  4. 根据Key通过哈希算法与与运算得出数组下标

  5. 如果数组下标位置元素为空,则将key和value封装为Entry对象 (JDK1.7中是Entry对象,JDK1.8中是Node对象) 并放入该位置
  6. 如果数组下标位置元素不为空,则要分情况讨论

    1. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry 对象,并使用头插法添加到当前位置的链表中
    2. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node

      1. 如果是红黑树Node,则将key和value封装为⼀个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
      2. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于等于8,那么则会将该链表转成红黑树
      3. 将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束PUT方法

        16、泛型中extends和super的区别

    3. <? extends T>表示包括T在内的任何T的子类

    4. <? super T>表示包括T在内的任何T的父类

17、深拷贝和浅拷贝

深拷贝和浅拷贝就是指对象的拷贝,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引用。

  1. 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制⼀份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同⼀个对象
  2. 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同⼀个对象

    18、HashMap的扩容机制原理

    1.7版本:

    1. 先生成新数组
    2. 遍历老数组中的每个位置上的链表上的每个元素
    3. 取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
    4. 将元素添加到新数组中去
    5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本:

  1. 先生成新数组
  2. 遍历老数组中的每个位置上的链表或红黑树
  3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
  4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
    1. 统计每个下标位置的元素个数
    2. 如果该位置下的元素个数超过了8,则生成⼀个新的红黑树,并将根节点的添加到新数组的对应位置
    3. 如果该位置下的元素个数没有超过8,那么则生成⼀个链表,并将链表的头节点添加到新数组的对应位置
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

    19、CopyOnWriteArrayList的底层原理是怎样的

  6. 首先CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制⼀个新的数组,写操作在新数组上进行,读操作在原数组上进行

  7. 并且,写操作会加锁,防止出现并发写入丢失数据的问题
  8. 写操作结束之后会把原数组指向新数组
  9. CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景

    20、什么是字节码?采用字节码的好处是什么?

    Java中的编译器和解释器:Java中引入了虚拟机的概念,即在机器和编译程序之间加入了⼀层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序⼀个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码 ( 即扩展名为 .class的文件) ,它不面向任何特定的处理器,只面向虚拟机。
    每⼀种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节 码,字节码由虚拟机解释执行,虚拟机将每⼀条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
    Java源代码 ——> 编译器 ——> jvm可执行的Java字节码(即虚拟指令) ——> jvm ——> jvm中解释器 ——-> 机器可执行的二进制机器码——>程序运行
    采用字节码的好处:Java语⾔通过字节码的方式,在⼀定程度上解决了传统解释型语⾔执行效率低的问题,同时又保留了解释型语⾔可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专 对⼀种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行

    21、Java中的异常体系是怎样的

  10. Java中的所有异常都来自顶级父类Throwable。

  11. Throwable下有两个子类Exception和Error。
  12. Error是程序无法处理的错误,⼀旦出现这个错误,则程序将被迫停止运行。
  13. Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常
  14. RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。
  15. CheckedException常常发生在程序编译过程中,会导致程序编译不通过。

    22、Java中有哪些类加载器

    JDK自带有三个类加载器:bootstrap ClassLoader 、ExtClassLoader 、AppClassLoader。

  16. BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件

  17. ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的 jar包和class类。
  18. AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。

    23、说说类加载器双亲委派模型

    JVM中存在三个默认的类加载器:

  19. BootstrapClassLoader

  20. ExtClassLoader
  21. AppClassLoader

AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BootstrapClassLoader
JVM在加载⼀个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会 先使用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先使用 BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果 BootstrapClassLoader没有加载到,那么ExtClassLoader就会自已尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自已进行加载。

24、GC如何判断对象可以被回收

  1. 引用计数法:每个对象有⼀个引用计数属性,新增⼀个引用时计数加1,引用释放时计数减1,计数为0时可以回收
  2. 可达性分析法:从GC Roots 开始向下搜索,搜索所⾛过的路径称为引用链。当⼀个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。

引用计数法,可能会出现A 引用了 B,B 又引用了 A,这时候就算他们都不再使用了,但因为相互引
用,计数器=1永远无法被回收。

25、GC Roots的对象有哪些

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即⼀般说的Native方法)引用的对象

可达性算法中的不可达对象并不是立即死亡的,对象拥有⼀次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第⼀次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由⼀低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”
每个对象只能触发⼀次finalize()方法
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。

26、JVM中哪些是线程共享区

堆区和方法区是所有线程共享的,栈、本地方法栈、程序计数器是每个线程独有的
image.png

27、你们项目如何排查JVM问题

对于还在正常运行的系统:

  1. 可以使用jmap来查看JVM中各个区域的使用情况
  2. 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
  3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
  4. 通过各个命令的结果,或者jvisualvm等工具来进行分析
  5. 首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又⼀直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
  6. 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存

对于已经发生了OOM的系统:

  1. ⼀般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件
    1. ( -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
  2. 我们可以利用jsisualvm等工具来分析dump文件
  3. 根据dump文件找到异常的实例对象,和异常的线程 (占用CPU高) ,定位到具体的代码
  4. 然后再进行详细的分析和调试

总之,调优不是⼀蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题

28、⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?

  1. 用户创建⼀个对象,JVM首先需要到方法区去找对象的类型信息。然后再创建对象。
  2. JVM要实例化⼀个对象,首先要在堆当中先创建⼀个对象。-> 半初始化状态
  3. 对象首先会分配在堆内存中新生代的Eden 。然后经过⼀次Minor GC,对象如果存活,就会进入S 区。在后续的每次GC中,如果对象⼀直存活,就会在S区来回拷贝,每移动⼀次,年龄加1 。-> 多大年龄才会移入老年代? 年龄最大15,超过⼀定年龄后,对象转入老年代。
  4. 当方法执行结束后,栈中的指针会先移除掉。
  5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。

    29、怎么确定⼀个对象到底是不是垃圾?

  6. 引用计数:这种方式是给堆内存当中的每个对象记录⼀个引用个数。引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。

  7. 根可达算法:这种方式是在内存中,从引用根对象向下⼀直找引用,找不到的对象就是垃圾。

    30、JVM有哪些垃圾回收算法?

  8. MarkSweep标记清除算法:这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片

  9. Copying 拷贝算法:为了解决标记清除算法的内存碎片问题,就产生了拷贝算法。拷贝算法将内存分为大小相等的两半,每次只使用其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷贝到另⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存货对象的个数有关。
  10. MarkCompack标记压缩算法:为了解决拷贝算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往⼀端移动,然后将端边界以外的所有内存直接清除。

这三种算法各有利弊,各自有各自的适合场景。

31、什么是STW?

STW:Stop-The-World,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的⼀种状态。在STW 状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。

32、JVM有哪些垃圾回收器?

  1. 新生代收集器:
    1. Serial
    2. ParNew
    3. Parallel Scavenge
  2. 老年代收集器:
    1. CMS
    2. Serial Old
    3. Parallel Old
  3. 整堆收集器:

    1. G1

      33、垃圾回收分为哪些阶段

      GC分为四个阶段:
      第⼀:初始标记标记出GCRoot直接引用的对象。STW
      第二:标记Region,通过RSet标记出上⼀个阶段标记的Region引用到的Old区Region。
      第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,而只需要遍历第二步标记出来的Region。
      第四:重新标记:跟CMS中的重新标记过程是差不多的。
      第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另⼀个Region 。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。

      34、什么是三色标记?

      三色标记:是⼀种逻辑上的抽象。将每个内存对象分成三种颜色:
  4. 黑色:表示自已和成员变量都已经标记完毕。

  5. 灰色:自已标记完了,但是成员变量还没有完全标记完。
  6. 白色:自已未标记完。

    35、JVM参数有哪些?

    JVM参数大致可以分为三类:

  7. 标注指令:-开头,这些是所有的HotSpot都支持的参数。可以用java -help 打印出来。

  8. 非标准指令:-X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java -X 打印出来。
  9. 不稳定参数:-XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化非常大。详细的文档资料非常少。在JDK1.8版本下,有几个常用的不稳定指令:

java -XX:+PrintCommandLineFlags :查看当前命令的不稳定指令。
java -XX:+PrintFlagsInitial :查看所有不稳定指令的默认值。
java -XX:+PrintFlagsFinal:查看所有不稳定指令最终生效的实际值。

二、Java并发(20)

1、线程的生命周期?线程有几种状态

线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态:

  1. 新建状态(New) :新创建了⼀个线程对象。
  2. 就绪状态( Runnable) :线程对象创建后,其他线程调用了该对象的start方法 。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态( Running) :就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态( Blocked) :阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行 。直到线程进入就绪状态,才有机会转到运行状态。
  5. 死亡状态( Dead) :线程执行完了或者因异常退出了run方法,该线程结束生命周期。

阻塞的情况又分为三种:

  1. 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源 ,JVM会把该线程放入“等待池”中 。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法
  2. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放 入“锁池”中。
  3. 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法

    2、sleep()、wait()、join()、yield()之间的的区别

    锁池:所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中⼀个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。
    等待池:当我们调用wait ( ) 方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁 。只 有调用了notify ( ) 或notifyAll()后等待池的线程才会开始去竞争锁,notify ( ) 是随机从等待池选出⼀个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中

  4. sleep 是Thread 类的静态本地方法,wait 则是Object 类的本地方法。

  5. sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
    1. sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与 cpu的调度,获取到cpu资源后就可以继续运行了。⽽如果sleep时该线程有锁,那么sleep不会释放这个锁,⽽是把锁带着进⼊了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt⽅法,那么这个线程也会抛出interruptexception异常返回,这点和wait是⼀样的。
  6. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
  7. sleep不需要被唤醒 ( 休眠之后推出阻塞) ,但是wait需要 ( 不指定时间需要被别人中断) 。
  8. sleep⼀般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信。
  9. sleep会让出CPU 执行时间且强制上下文切换,而wait 则不⼀定,wait后可能还是有机会重新竞争到锁继续执行的
  10. yield( ) 执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
  11. join ( ) 执行后线程进入阻塞状态,例如在线程B中调用线程A的join ( ),那线程B会进入到阻塞队列,直到线程A结束或中断线程 ```java public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() {
    1. @Override public void run() {
    2. try {
    3. Thread.sleep(3000);
    4. }
    5. catch (InterruptedException e) {
    6. e.printStackTrace();
    7. }
    8. System.out.println("22222222");
    9. }
    }); t1.start(); t1.join(); // 这⾏代码必须要等t1全部执⾏完毕,才会执⾏ System.out.println(“1111”); }

22222222 1111

  1. <a name="m66IK"></a>
  2. ## 3、对线程安全的理解
  3. 不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问,当多个线程访问⼀个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。<br />堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。在Java中,堆是Java虚拟机所管理的内存中最大的⼀块,是所有线程共享的⼀块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯⼀目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。<br />栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语⾔里面显式的分配和释放。<br />目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自已的内存空间,而不能访问别的进程的,这是由操作系统保障的。<br />在每个进程的内存空间中都会有⼀块特殊的公共区域,通常称为堆 ( 内存) 。进程内的所有线程都可以 访问到该区域,这就是造成问题的潜在原因。
  4. <a name="O7tF0"></a>
  5. ## 4、Thread和Runable的区别
  6. Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行⼀个任务,那就实现runnable。
  7. ```java
  8. //会卖出多⼀倍的票
  9. public class Test {
  10. public static void main(String[] args) {
  11. // TODO Auto-generated method stub
  12. new MyThread().start();
  13. new MyThread().start();
  14. }
  15. static class MyThread extends Thread{
  16. private int ticket = 5;
  17. public void run(){
  18. while(true){
  19. System.out.println("Thread ticket = " + ticket--);
  20. if(ticket < 0){
  21. break;
  22. }
  23. }
  24. }
  25. }
  26. }
  1. //正常卖出
  2. public class Test2 {
  3. public static void main(String[] args) {
  4. // TODO Auto-generated method stub
  5. MyThread2 mt = new MyThread2();
  6. new Thread(mt).start();
  7. new Thread(mt).start();
  8. }
  9. static class MyThread2 implements Runnable{
  10. private int ticket = 5;
  11. public void run(){
  12. while(true){
  13. System.out.println("Thread ticket = " + ticket--);
  14. if(ticket < 0){
  15. break;
  16. }
  17. }
  18. }
  19. }
  20. }

原因是:MyThread创建了两个实例,自然会卖出两倍,属于用法错误

5、对守护线程的理解

守护线程:为所有非守护线程提供服务的线程;任何⼀个守护线程都是整个JVM中所有非守护线程的保姆;
守护线程类似于整个进程的⼀个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把IO 、File等重要操作逻辑分配给它;因 为它不靠谱;
守护线程的作用是什么?
举例,GC垃圾回收线程:就是⼀个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就 不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:

  1. 来为其它线程提供服务支持的情况;
  2. 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果⼀个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。

thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出⼀个IllegalThreadStateException 异常 。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在⼀个操作的中间发生中断。
Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台 线程就不能用Java的线程池。

6、ThreadLocal的底层原理

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
  3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key ,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过 强引用指向ThreadLocalMap ,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象
  4. ThreadLocal经典的应用场景就是连接管理 (⼀个线程持有⼀个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同⼀个连接)

    7、并发、并行、串行之间的区别

  5. 串行在时间上不可能发生重叠,前⼀个任务没搞定,下⼀个任务就只能等着

  6. 并行在时间上是重叠的,两个任务在同⼀时刻互不干扰的同时执行。
  7. 并发允许两个任务彼此干扰。统⼀时间点、只有⼀个任务运行,交替执行

    8、并发的三大特性

    8.1 原子性:
    原子性是指在⼀个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要 不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。 ```java private long count = 0;

public void calc() { count++; }

  1. 1. count 从主存读到工作内存中的副本中
  2. 1. +1的运算
  3. 1. 将结果写入工作内存
  4. 1. 将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)
  5. 那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的 ,包括读取变量的原始值、进行加1操作 、写入工作内存 。所以在多线程中,有可能⼀个线程还没自增完,可能才执行到第二部,另⼀个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是⼀个原子性的操作,那么就能保证其他线程读取到的⼀定是自增后的数据。<br />**关键字:synchronized**
  6. **8.2 可见性**<br />当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够立即看得到修改的值。<br />若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还 是之前的,线程1对变量的修改线程没看到这就是可见性问题。
  7. ```java
  8. // 线程1
  9. boolean stop = false;
  10. while(!stop){
  11. doSomething();
  12. }
  13. // 线程2
  14. stop = true;

如果线程2改变了stop的值,线程1⼀定会停止吗?不⼀定 。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会⼀直循环下去。
关键字:volatile 、synchronized 、final

8.3 有序性
虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不⼀定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。

  1. int a = 0;
  2. bool flag = false;
  3. public void write() {
  4. a = 2; //1
  5. flag = true; //2
  6. }
  7. public void multiply() {
  8. if (flag) { //3
  9. int ret = a * a;//4
  10. }
  11. }

write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再 到线程1,这时候a才赋值为2,很明显迟了⼀步
关键字:volatile 、synchronized

volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由“⼀个变量在同⼀时刻只允许⼀条线程对其进行lock操作”这条规则明确的。

synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。

在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或
java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。

我们判断使用volatile还是加锁的唯⼀依据就是volatile的语义能否满足使用的场景(原子性)

9、Java死锁如何避免?

造成死锁的几个原因:

  1. ⼀个资源每次只能被⼀个线程使用
  2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源
  3. ⼀个线程已经获得的资源,在未使用完之前,不能被强行剥夺
  4. 若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某⼀个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
  2. 要注意加锁时限,可以针对所设置⼀个超时时间
  3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进行解决

    10、如何理解volatile关键字

    保证被volatile修饰的共享变量对所有线程总是可见的,也就是当⼀个线程修改了⼀个被volatile修饰共 享变量的值,新值总是可以被其他线程立即得知。
    如果线程2改变了stop的值,线程1⼀定会停止吗?不⼀定。当线程2更改了stop变量的值之后,但是还 没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会⼀直循环下去。 ```java //线程1 boolean stop = false; while(!stop){ doSomething(); }

//线程2 stop = true;

  1. 禁止指令重排序优化
  2. ```java
  3. int a = 0;
  4. bool flag = false;
  5. public void write() {
  6. a = 2; //1
  7. flag = true; //2
  8. }
  9. public void multiply() {
  10. if (flag) { //3
  11. int ret = a * a;//4
  12. }
  13. }

write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再 到线程1,这时候a才赋值为2,很明显迟了⼀步 。但是用volatile修饰之后就变得不⼀样了:

  1. 使用volatile关键字会强制将修改的值立即写入主存;
  2. 使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效 ( 反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效) ;
  3. 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取

inc++; 其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以volatile不能保证线程安全。

11、为什么用线程池?解释下线程池参数?

  1. 降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
  2. 提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行 。
  3. 提高线程的可管理性;线程是稀缺资源,使用线程池可以统⼀分配调优监控。

线程池参数:

  1. corePoolSize代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是⼀种常驻线程
  2. maxinumPoolSize代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数
  3. keepAliveTime、unit表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲⼀定的时间则会被消除,我们可以通过setKeepAliveTime来设置空闲时间
  4. workQueue用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
  5. ThreadFactory实际上是⼀个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同⼀个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,⼀般我们会根据业务来制定不同的线程工厂
  6. Handler任务拒绝策略,有两种情况,第⼀种是当我们调用shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另⼀种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝

    12、线程池的底层工作原理

    线程池内部是通过队列 + 线程实现的,当我们利用线程池执行任务时:

  7. 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

  8. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  9. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  10. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  11. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

    13、线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?

  12. ⼀般的队列只能保证作为⼀个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。

    1. 阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资 源。
    2. 阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于⼀直占用cpu资源
  13. 在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

    1. 就好比⼀个企业里面有10个 ( core) 正式工的名额,最多招10个正式工,要是任务超过正式工人数 (task > core) 的情况下,工厂领导 ( 线程池) 不是首先扩招工人,还是这10人,但是任务可以稍微积压⼀下,即先放到队列去 ( 代价低) 。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了 ( 队列满了) ,就的招外包帮忙了 ( 注意是临时工) 要是正式工加上外包还 是不能完成任务,那新来的任务就会被领导拒绝了 ( 线程池的拒绝策略) 。

      14、线程池中线程复用原理

      线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread 创建线程时的⼀个 线程必须对应⼀个任务的限制。
      在线程池中,同⼀个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用Thread.start() 来创建新线程,而是让每个线程去执行⼀个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run 方法,将run 方法当成⼀个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。

      15、ReentrantLock中的公平锁和非公平锁的底层实现

      首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使 用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队 ,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
      不管是公平锁还是非公平锁,⼀旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。
      另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

      16、ReentrantLock中tryLock()和lock()方法的区别

  14. tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回 true,没有加到则返回false

  15. lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

    17、CountDownLatch和Semaphore的区别和底层原理

    CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调用 CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对 CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。
    对应的底层原理就是,调用await()方法的线程会利用AQS排队,⼀旦数字被减为0,则会将AQS中排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release() 方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。

18、Sychronized的偏向锁、轻量级锁、重量级锁

  1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
  2. 轻量级锁:由偏向锁升级而来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
  3. 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运行中,相对而⾔没有使用太多的操作系统资源,比较轻量。

    19、Sychronized和ReentrantLock的区别

  5. sychronized是⼀个关键字,ReentrantLock是⼀个类

  6. sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
  7. sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
  8. sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
  9. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
  10. sychronized底层有⼀个锁升级的过程

    20、谈谈你对AQS的理解,AQS如何实现可重入锁?

  11. AQS是⼀个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。

  12. 在AQS中,维护了⼀个信号量state和⼀个线程组成的双向链表队列 。其中,这个线程队列,就是用来给线程排队的,而state就像是⼀个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不用的意义。
  13. 在可重入锁这个场景下,state就用来表示加锁的次数 。0标识无锁,每加⼀次锁,state就加1。释放锁state就减1。

    三、开发框架(28)

    1、Spring是什么?

    轻量级的开源的J2EE框架。它是⼀个容器框架,用来装javabean(java对象),中间层框架(万能胶)可以起⼀个连接作用,比如说把Struts和hibernate粘合在⼀起运用 ,可以让我们的企业开发更快、更简 洁,Spring是⼀个轻量级的控制反转 (IoC)和面向切面 (AOP) 的容器框架:

  14. 从大小与开销两方面而⾔Spring都是轻量级的。

  15. 通过控制反转(IoC)的技术达到松耦合的目的
  16. 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发
  17. 包含并管理应用对象(Bean)的配置和生命周期,这个意义上是⼀个容器。
  18. 将简单的组件配置、组合成为复杂的应用,这个意义上是⼀个框架。

    2、谈谈你对AOP的理解

    系统是由许多不同的组件所组成的,每⼀个组件各负责⼀块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。
    当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上 到下的关系,但并不适合定义从左到右的关系。例如日志功能。
    日志代码往往⽔平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。
    在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
    AOP:将程序中的交叉业务逻辑 ( 比如安全、日志、事务等) ,封装成⼀个切面,然后注入到目标对象 ( 具体业务逻辑) 中去 。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增 强,可以在执行某个方法之前额外的做⼀些事情,在某个方法执行之后额外的做⼀些事情

    3、谈谈你对IOC的理解

    容器概念、控制反转、依赖注入
    ioc容器:实际上就是个map ( key ,value) ,里面存的是各种对象 ( 在xml里配置的bean节点、 @repository 、@service 、@controller 、@component) ,在项目启动的时候会读取配置文件里面的 bean节点,根据全限定类名使用反射创建对象放到map里、扫描到打上上述注解的类还是通过反射创建对象放到map里。
    这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入 ( autowired 、resource等注解,xml里bean节点内的ref属性,项目启动的时候会读取xml节点ref属性 根据id注入,也会扫描这些注解,根据类型或id注入;id就是对象名) 。

控制反转:
没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某⼀点的时候,自已必须 主动去创建对象B或者使用已经创建的对象B 。无论是创建还是使用对象B,控制权都在自已手上。
引入IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会 主动创建⼀个对象B注入到对象A需要的地方。
通过前后的对比,不难看出来:对象A获得依赖对象B的过程, 由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。

全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了 ⼀种类似“粘合剂”的作用 ,把系统中的所有对象粘合在⼀起发挥作用 ,如果没有这个“粘合剂”,对象与 对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。

依赖注入:
“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器 主动注入。依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。

4、解释下Spring支持的几种bean的作用域

  1. singleton:默认,每个容器中只有⼀个bean的实例,单例的模式由BeanFactory自身来维护 。该对象的生命周期是与Spring IOC容器⼀致的 ( 但在第⼀次被注入时才会创建) 。
  2. prototype:为每⼀个bean请求提供⼀个实例。在每次注入时都会创建⼀个新的对象
  3. request:bean被定义为在每个HTTP请求中创建⼀个单例对象,也就是说在单个请求中都会复用这⼀个单例对象。
  4. session:与request范围类似,确保每个session中有⼀个bean的实例,在session过期后,bean会随之失效。
  5. application:bean被定义为在ServletContext的生命周期中复用⼀个单例对象。
  6. websocket:bean被定义为在websocket的生命周期中复用⼀个单例对象 。
  7. global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet 。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这 全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。

    5、Spring事务的实现方式和原理以及隔离级别?

    在使用Spring框架时,可以有两种使用事务的方式,⼀种是编程式的,⼀种是申明式的,
    @Transactional注解就是申明式的。

首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了⼀些能让程序员更加方便操作事务的方式。

比如我们可以通过在某个方法上增加@Transactional注解,就可以开启事务,这个方法中所有的sql都会在⼀个事务中执行,统⼀成功或失败。

在⼀个方法上加了@Transactional注解后,Spring会基于这个类生成⼀个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法上存在@Transactional注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务进行回滚。

当然,针对哪些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbackFor属性进行配 置,默认情况下会对RuntimeException和Error进行回滚。

Spring事务隔离级别就是数据库的隔离级别:外加⼀个默认级别

  1. read uncommitted ( 未提交读)
  2. read committed ( 提交读、不可重复读)
  3. repeatable read ( 可重复读)
  4. serializable ( 可串行化)

  5. 数据库的配置隔离级别是Read Commited,而Spring配置的隔离级别是Repeatable Read,请问这时隔离级别是以哪⼀个为准?

  6. 以Spring配置的为准,如果Spring设置的隔离级别数据库不支持,效果取决于数据库

    6、Spring事务传播机制

    多个事务方法相互调用时,事务如何在这些方法间传播,方法A是⼀个事务的方法,方法A执行过程中调 用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。

  7. REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自已新建⼀个事务,如果当前存在事务,则加入这个事务

  8. SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
  9. MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
  10. REQUIRES_NEW:创建⼀个新事务,如果存在当前事务,则挂起该事务。
  11. NOT_SUPPORTED:以非事务方式执行 ,如果当前存在事务,则挂起当前事务
  12. NEVER:不使用事务,如果当前事务存在,则抛出异常
  13. NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作⼀样(开启⼀个事务)

    7、Spring事务什么时候会失效?

    Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有 如下几种

  14. 发生自调用 ,类里面使用this调用本类的方法 (this通常省略) ,此时这个this对象不是代理类,而是 UserService对象本身!

    1. 解决方法很简单,让那个this变成UserService的代理类即可!
  15. 方法不是public的:@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非public 方法上,可以开启 AspectJ 代理模式。
  16. 数据库不支持事务
  17. 没有被Spring管理
  18. 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

    8、什么是bean的自动装配,有哪些方式?

    开启自动装配,只需要在xml配置文件中定义“autowire”属性。

    1. <bean id="cutomer" class="com.xxx.xxx.Customer" autowire="" />

    autowire属性有五种装配的方式:

  19. no – 缺省情况下,自动配置是通过“ref”属性手动设定

    1. ⼿动装配:以value或ref的⽅式明确指定属性值都是⼿动装配。
    2. 需要通过‘ref’属性来连接bean。
  20. byName-根据bean的属性名称进行自动装配。

    1. Cutomer的属性名称是personSpring会将bean idpersonbean通过setter方法进行自动装配。
    2. <bean id="cutomer" class="com.xxx.xxx.Cutomer" autowire="byName"/>
    3. <bean id="person" class="com.xxx.xxx.Person"/>
  21. byType-根据bean的类型进行自动装配。

    1. Cutomer的属性person的类型为PersonSpirng会将Person类型通过setter方法进行自动装配。
    2. <bean id="cutomer" class="com.xxx.xxx.Cutomer" autowire="byType"/>
    3. <bean id="person" class="com.xxx.xxx.Person"/>
  22. constructor-类似byType,不过是应用于构造器的参数。如果⼀个bean与构造器参数的类型形同,则进行自动装配,否则导致异常。

    1. Cutomer构造函数的参数person的类型为PersonSpirng会将Person类型通过构造方法进行自动装配。
    2. <bean id="cutomer" class="com.xxx.xxx.Cutomer" autowire="construtor"/>
    3. <bean id="person" class="com.xxx.xxx.Person"/>
  23. autodetect-如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配。

    1. 如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配

@Autowired自动装配bean,可以在字段 、setter方法、构造函数上使用。

9、Spring中的Bean创建的生命周期有哪些步骤

Spring中⼀个Bean的创建大概分为以下几个步骤:

  1. 推断构造方法
  2. 实例化
  3. 填充属性,也就是依赖注入
  4. 处理Aware回调
  5. 初始化前,处理@PostConstruct注解
  6. 初始化,处理InitializingBean接口
  7. 初始化后,进行AOP

当然其实真正的步骤更加细致,可以看下面的流程图
image.png

10、Spring中Bean是线程安全的吗

Spring本身并没有针对Bean做线程安全的处理,所以:

  1. 如果Bean是无状态的,那么Bean则是线程安全的
  2. 如果Bean是有状态的,那么Bean则不是线程安全的

另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是⼀个对象,这个对象是不是线程安全的,还是得看这个Bean对象本 身。

11、ApplicationContext和BeanFactory有什么区别

BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而 ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也 是⼀个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如 EnvironmentCapable 、MessageSource 、ApplicationEventPublisher等接口,从而 ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的

12、Spring中的事务是如何实现的

  1. Spring事务底层是基于数据库事务和AOP机制的
  2. 首先对于使用了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
  3. 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
  4. 如果加了,那么则利用事务管理器创建⼀个数据库连接
  5. 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的⼀步
  6. 然后执行当前方法,方法中会执行sql
  7. 执行完当前方法后,如果没有出现异常就直接提交事务
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  9. Spring事务的隔离级别对应的就是数据库的隔离级别
  10. Spring事务的传播机制是Spring事务自已实现的,也是Spring事务中最复杂的
  11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要新开⼀个事务,那么实际上就是先建立⼀个数据库连接,在此新数据库连接上执行sql

    13、Spring中什么时候@Transactional会失效

    因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会失效的。
    同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类来实现 的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效

    14、Spring容器启动流程是怎样的

  12. 在创建Spring容器,也就是启动Spring时:

  13. 首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
  14. 然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建
  15. 利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition 、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化 后这⼀步骤中
  16. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
  17. Spring启动结束
  18. 在源码中会更复杂,比如源码中会提供⼀些模板方法,让子类来实现,比如源码中还涉及到⼀些 BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过 BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
  19. 在Spring启动过程中还会去处理@Import等注解

    15、Spring用到了哪些设计模式

    image.png

    16、Spring Boot、Spring MVC 和Spring 有什么区别

    Spring是⼀个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提 供AOP机制弥补OOP的代码重复问题、更方便将不同类不同方法中的共同处理抽取成切面、 自动注入给方法执行,比如日志、异常等
    Springmvc是Spring对web框架的⼀个解决方案,提供了⼀个总的前端控制器Servlet,用来接收请求,然后定义了⼀套路由策略 ( url到handle的映射) 及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端
    Springboot是Spring提供的⼀个快速开发工具包,让程序员能更方便、更快速的开发Spring+Springmvc应用,简化了配置(约定了默认配置),整合了⼀系列的解决方案(starter机制)、Redis、mongodb、es,可以开箱即用

    17、Spring MVC 工作流程

    1) 用户发送请求至前端控制器 DispatcherServlet。
    2) DispatcherServlet 收到请求调用HandlerMapping 处理器映射器。
    3) 处理器映射器找到具体的处理器(可以根据 xml 配置、注解进行查找),生成处理器及处理器拦截器(如果有则生成)⼀并返回给 DispatcherServlet。
    4) DispatcherServlet 调用HandlerAdapter 处理器适配器。
    5) HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)
    6) Controller 执行完成返回ModelAndView。
    7) HandlerAdapter 将controller 执行结果 ModelAndView 返回给 DispatcherServlet 。
    8) DispatcherServlet 将 ModelAndView 传给ViewReslover 视图解析器。
    9) ViewReslover 解析后返回具体View。
    10) DispatcherServlet 根据View 进行渲染视图 ( 即将模型数据填充至视图中) 。
    11) DispatcherServlet 响应用户。

    18、Spring MVC的主要组件?

    Handler:也就是处理器 。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。在Controller层中@RequestMapping标注的所有方法都可以看成是⼀个 Handler,只要可以实际处理请求就可以是Handler
    1、HandlerMapping
    initHandlerMappings(context),处理器映射器,根据用户请求的资源uri来查找Handler的 。在 SpringMVC中会有很多请求,每个请求都需要⼀个Handler处理,具体接收到⼀个请求之后使用哪个 Handler进行,这就是HandlerMapping需要做的事。
    2、HandlerAdapter
    initHandlerAdapters(context),适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情 。 Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。
    3、HandlerExceptionResolver
    initHandlerExceptionResolvers(context),其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢? 这就需要有⼀个专门的⻆色对异常情况进行处理,在SpringMVC中就是 HandlerExceptionResolver 。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给 render方法进行渲染。
    4、ViewResolver
    initViewResolvers(context) ,ViewResolver用来将String类型的视图名和Locale解析为View类型的视 图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html ( 也可能是其它类型) 文 件。这里就有两个关键问题:使用哪个模板?用什么技术 ( 规则) 填入参数? 这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自已完成。
    5、RequestToViewNameTranslator
    initRequestToViewNameTranslator(context) ,ViewResolver是根据ViewName查找View,但有的 Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。
    RequestToViewNameTranslator在Spring MVC容器里只可以配置⼀个,所以所有request到 ViewName的转换规则都要在⼀个Translator里面全部实现。
    6、LocaleResolver
    initLocaleResolver(context),解析视图需要两个参数:⼀是视图名,另⼀个是Locale 。视图名是处理 器返回的,Locale是从哪里来的? 这就是LocaleResolver要做的事情。LocaleResolver用于从request 解析出Locale,Locale就是zh-cn之类,表示⼀个区域,有了这个就可以对不同区域的用户显示不同的结果。
    SpringMVC主要有两个地方用到了Locale:⼀是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。
    7、ThemeResolver
    initThemeResolver(context),用于解析主题 。SpringMVC中⼀个主题对应⼀个properties文件,里面 存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同⼀个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有ThemeResolver 、ThemeSource 和Theme。主题是通过⼀系列资源来具体体现的,要得到⼀个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为⼀个配置)文件,这是 ThemeSource的工作 。最后从主题中获取资源就可以了。
    8、MultipartResolver
    initMultipartResolver(context),用于处理上传请求。处理方法是将普通的request包装成 MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调 用getFileMap得到FileName->File结构的Map 。此组件中⼀共有三个方法,作用分别是判断是不是上传 请求,将request包装成MultipartHttpServletRequest 、处理完后清理上传过程中产生的临时资源。
    9、FlashMapManager
    initFlashMapManager(context),用来管理FlashMap的,FlashMap主要用在redirect中传递参数。

    19、Spring Boot自动配置原理?

    @Import + @Configuration + Spring spi
    自动配置类由各个starter提供,使用@Configuration + @Bean定义配置类,放到META-INF/Spring.factories下
    使用Spring spi扫描META-INF/Spring.factories下的配置类
    使用@Import导入自动配置类

    20、如何理解Spring Boot 中的Starter

    使用Spring + Springmvc使用 ,如果需要引入mybatis等框架,需要到xml中定义mybatis需要的bean
    starter就是定义⼀个starter的jar包,写⼀个@Configuration配置类 、将这些bean定义在里面,然后在 starter包的META-INF/Spring.factories中写入该配置类,Springboot会按照约定来加载该配置类
    开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了,比如mybatis-Spring-boot—starter,Spring- boot-starter-Redis

    21、什么是嵌入式服务器?为什么要使用嵌入式服务器?

  20. 节省了下载安装tomcat,应用也不需要再打war包,然后放到webapp目录下再运行

  21. 只需要⼀个安装了Java 的虚拟机,就可以直接在上面部署应用程序了
  22. Springboot已经内置了tomcat.jar,运行main方法时会去启动tomcat,并利用tomcat的spi机制加载 Springmvc

    22、Spring Boot中常用注解及其底层实现

  23. @SpringBootApplication注解:这个注解标识了⼀个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:

    1. @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个配置类
    2. @EnableAutoConfiguration:向Spring容器中导入了⼀个Selector,用来加载ClassPath下 SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean
    3. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
  24. @Bean注解:用来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象
  25. @Controller 、@Service 、@ResponseBody 、@Autowired都可以说

    23、Spring Boot是如何启动Tomcat的

  26. 首先,SpringBoot在启动时会先创建⼀个Spring容器

  27. 在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成⼀个启动Tomcat的Bean
  28. Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat

    24、Spring Boot中配置文件的加载顺序是怎样的?

    优先级从高到低,高优先级的配置覆盖低优先级的配置,所有配置会形成互补配置。

  29. 命令行参数。所有的配置都可以在命令行上进行指定;

  30. Java系统属性(System.getProperties());
  31. 操作系统环境变量;
  32. jar包外部的application-{profile}.properties或application.yml(带Spring.profile)配置文件
  33. jar包内部的application-{profile}.properties或application.yml(带Spring.profile)配置文件再来加载不带profile
  34. jar包外部的application.properties或application.yml(不带Spring.profile)配置文件
  35. jar包内部的application.properties或application.yml(不带Spring.profile)配置文件
  36. @Configuration注解类上的@PropertySource

    25、Mybatis的优缺点

    优点:

  37. 基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML 里,解除sql与程序代码的耦合,便于统⼀管理;提供 XML 标签,支持编写动态SQL语句,并可重用。

  38. 与JDBC 相比,减少了 50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
  39. 很好的与各种数据库兼容 ( 因为 MyBatis 使用JDBC 来连接数据库,所以只要JDBC 支持的数据库 MyBatis 都支持) 。
  40. 能够与Spring 很好的集成;
  41. 提供映射标签,支持对象与数据库的ORM 字段关系映射; 提供对象关系映射标签,支持对象关系组件维护。

缺点:

  1. SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL 语句的功底有⼀定要求
  2. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

    26、MyBatis 与Hibernate 有哪些不同?

    SQL 和ORM 的争论,永远都不会终止
    开发速度的对比:
    Hibernate的真正掌握要比Mybatis难些。Mybatis框架相对简单很容易上手,但也相对简陋些。
    比起两者的开发速度,不仅仅要考虑到两者的特性及性能,更要根据项目需求去考虑究竟哪⼀个更适合项目开发,比如:⼀个项目中用到的复杂查询基本没有,就是简单的增删改查,这样选择hibernate效率 就很快了,因为基本的sql语句已经被封装好了,根本不需要你去写sql语句,这就节省了大量的时间 ,但是对于⼀个大型项目,复杂语句较多,这样再去选择hibernate就不是⼀个太好的选择,选择mybatis 就会加快许多,而且语句的管理也比较方便。

开发工作量的对比:
Hibernate和MyBatis都有相应的代码生成工具。可以生成简单基本的DAO层方法 。针对高级查询,Mybatis需要手动编写SQL语句,以及ResultMap 。而Hibernate有良好的映射机制,开发者无需关心 SQL的生成与结果映射,可以更专注于业务流程
sql优化方面:
Hibernate的查询会将表中的所有字段查询出来,这⼀点会有性能消耗。Hibernate也可以自已写SQL来 指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。而Mybatis的SQL是手动编写的,所以 可以按需求指定查询的字段。
Hibernate HQL语句的调优需要将SQL打印出来,而Hibernate的SQL被很多人嫌弃因为太丑了 。 MyBatis的SQL是自已手动写的所以调整方便。但Hibernate具有自已的日志统计。Mybatis本身不带日志统计,使用Log4j进行日志记录。

对象管理的对比:
Hibernate 是完整的对象/关系映射解决方案,它提供了对象状态管理 ( state management) 的功能,使开发者不再需要理会底层数据库系统的细节。也就是说,相对于常见的JDBC/SQL 持久层方案中需 要管理SQL 语句,Hibernate采用了更自然的面向对象的视⻆来持久化Java 应用中的数据。
换句话说,使用 Hibernate 的开发者应该总是关注对象的状态( state) ,不必考虑 SQL 语句的执行 。 这部分细节已经由Hibernate 掌管妥当,只有开发者在进行系统性能调优的时候才需要进行了解。而 MyBatis在这⼀块没有文档说明,用户需要对对象自已进行详细的管理。

缓存机制对比:
相同点:都可以实现自已的缓存或使用其他第三方缓存方案,创建适配器来完全覆盖缓存行为。
不同点:Hibernate的二级缓存配置在SessionFactory生成的配置文件中进行详细配置,然后再在具体 的表-对象映射中配置是哪种缓存。
MyBatis的二级缓存配置都是在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义 不同的缓存机制。并且Mybatis可以在命名空间中共享相同的缓存配置和实例,通过Cache-ref来实 现。
两者比较:因为Hibernate对查询对象有着良好的管理机制,用户无需关心SQL 。所以在使用二级缓存 时如果出现脏数据,系统会报出错误并提示。
而MyBatis在这⼀方面,使用二级缓存时需要特别小心。如果不能完全确定数据更新操作的波及范围,避免Cache的盲目使用。否则,脏数据的出现会给系统的正常运行带来很大的隐患。
Hibernate功能强大,数据库无关性好,O/R映射能力强,如果你对Hibernate相当精通,而且对 Hibernate进行了适当的封装,那么你的项目整个持久层代码会相当简单,需要写的代码很少,开发速 度很快,非常爽。
Hibernate的缺点就是学习门槛不低,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间 如何权衡取得平衡,以及怎样用好Hibernate方面需要你的经验和能力都很强才行 。
iBATIS入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经 验,对于没有那么高的对象模型要求的项目来说,相当完美。
iBATIS的缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库 查询实际还是要自已写的,工作量也比较大,而且不太容易适应快速数据库修改。

27、#{}和${}的区别是什么?

{}是预编译处理、是占位符,${}是字符串替换 、是拼接符。
Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 来赋值;
Mybatis 在处理时,就是把{}替换成变量的值,调用 Statement 来赋值;
#{} 的变量替换是在DBMS 中、变量替换后,#{} 对应的变量自动加上单引号 ,{} 的变量替换是在 DBMS 外、变量替换后,{} 对应的变量不会加上单引号
使用#{}可以有效的防止 SQL 注入,提高系统安全性。

28、简述Mybatis 的插件运行原理,如何编写⼀个插件

Mybatis只支持针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis 使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke() 方法,拦截那些你指定需要拦截的方法。
编写插件:实现 Mybatis 的Interceptor 接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪⼀个接口的哪些方法即可,在配置文件中配置编写的插件。

  1. @Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
  2. @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
  3. @Signature(type = StatementHandler.class, method = "batch", args = { Statement.class })})
  4. @Component
  5. invocation.proceed() 执⾏具体的业务逻辑

四、Mysql(20)

1、索引的基本原理

索引用来快速地寻找那些具有特定值的记录。如果没有索引,⼀般来说执行查询时遍历整张表。
索引的原理:就是把无序的数据变成有序的查询

  1. 把创建了索引的列的内容进行排序
  2. 对排序结果生成倒排表
  3. 在倒排表内容上拼上数据地址链
  4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

    2、Mysql聚簇和非聚簇索引的区别

    都是B+树的数据结构
  1. 聚簇索引:将数据存储与索引放到了⼀块、并且是按照⼀定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是⼀致的,即:只要索引是相邻的,那么对应的数据⼀定也是相邻地存放在磁盘上的
  2. 非聚簇索引:叶子节点不存储数据、存储的是数据行地址,也就是说根据索引查找到数据行的位置再取磁盘查找数据,这个就有点类似⼀本树的目录,比如我们要找第三章第⼀节,那我们先在这个目录里面找,找到对应的页码后再去对应的页码看文章。

优势:

  1. 查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率要高
  2. 聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的
  3. 聚簇索引适合用在排序的场合,非聚簇索引不适合

劣势:

  1. 维护索引很昂贵,特别是插入新行或者主键被更新导至要分页(page split)的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZE TABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片
  2. 表因为使用UUId ( 随机ID) 作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更慢,所以建议使用int的auto_increment作为主键
  3. 如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值;过长的主键值,会导致非叶子节点占用占用更多的物理空间

InnoDB中⼀定有主键,主键⼀定是聚簇索引,不手动设置、则会使用unique索引,没有unique索引,则会使用数据库内部的⼀个行的隐藏id来当作主键索引。在聚簇索引之上创建的索引称之为辅助索引,
辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯⼀索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值
MyISM使用的是非聚簇索引,没有聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完 全⼀致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键 。 表数据存储在独立的地方,这两颗B+树的叶子节点都使用⼀个地址指向真正的表数据,对于表数据来 说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MyISAM占优势些,因为索引所占 空间小,这些操作是需要在内存中完成的。

3、Mysql索引的数据结构,各自优劣

索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等 ,InnoDB存储引擎的默认索引实现为:B+树索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
B+树:B+树是⼀个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互链接。在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波 动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库、文件系统等场景。
哈希索引:哈希索引就是采用⼀定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需⼀次哈希算法即可立刻定位到相应的位置,速度非常快
如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过⼀次算法即可找到相应的键值;前提是键值都是唯⼀的。如果键值不是唯⼀的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;
如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;
哈希索引也没办法利用索引完成排序,以及like‘xxx%’这样的部分模糊查询 ( 这种部分模糊查询,其实 本质上也是范围查询) ;
哈希索引也不支持多列联合索引的最左匹配规则;
B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在哈希碰撞问题。

4、索引设计的原则?

查询更快、占用空间更小

  1. 适合索引的列是出现在where子句中的列,或者连接子句中指定的列
  2. 基数较小的表,索引效果较差,没有必要在此列建立索引
  3. 使用短索引,如果对长字符串列进行索引,应该指定⼀个前缀长度,这样能够节省大量索引空间,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。
  4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。
  5. 定义有外键的数据列⼀定要建立索引。
  6. 更新频繁字段不适合创建索引
  7. 若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
  8. 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
  9. 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
  10. 对于定义为text 、image和bit的数据类型的列不要建立索引。

    5、InnoDB存储引擎的锁的算法

    1. Record lock:单个行记录上的锁
    2. Gap lock:间隙锁,锁定⼀个范围,不包括记录本身
    3. Next-key lock:record+gap 锁定⼀个范围,包含记录本身

相关知识点:

  1. innodb对于行的查询使用next-key lock
  2. Next-locking keying为了解决Phantom Problem幻读问题
  3. 当查询的索引含有唯⼀属性时,将next-key lock降级为record key
  4. Gap锁设计的目的是为了阻止多个事务将记录插入到同⼀范围内,而这会导致幻读问题的产生
  5. 有两种方式显式关闭gap锁: (除了外键约束和唯⼀性检查外,其余情况仅使用record lock)
    1. 将事务隔离级别设置为RC
    2. 将参数innodb_locks_unsafe_for_binlog设置为1

      6、关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?

      在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
      慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引? 是load了不需要的数据列?还是数据量太大?
      所以优化也是针对这三个方向来的,
      首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
      分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
      如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

      7、事务的基本特性和隔离级别

      事务基本特性ACID分别是:
      原子性指的是⼀个事务中的操作要么全部成功,要么全部失败。
      ⼀致性指的是数据库总是从⼀个⼀致性的状态转换到另外⼀个⼀致性的状态。比如A转账给B100块钱 ,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库 数据就破坏约束了,因此事务不能成功,这里我们说事务提供了⼀致性的保证
      隔离性指的是⼀个事务的修改在最终提交前,对其他事务是不可见的。
      持久性指的是⼀旦事务提交,所做的修改就会永久保存到数据库中。
      隔离性有4个隔离级别,分别是:
  1. read uncommit读未提交,可能会读到其他事务未提交的数据,也叫做脏读
    1. 用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读 取结果age=20,这就是脏读。
  2. read commit读已提交,两次读取结果不⼀致,叫做不可重复读
    1. 不可重复读解决了脏读的问题,他只会读取已经提交的事务。
    2. 用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同⼀个事务里同⼀个查询 读取到不同的结果叫做不可重复读。
  3. repeatable read可重复复读,这是mysql的默认级别,就是每次读取结果都⼀样,但是有可能产生幻读
  4. serializable串行,⼀般是不会使用的,他会给每⼀行读取的数据加锁,会导致大量超时和锁竞争的问题。

  5. 脏读(Drity Read):某个事务已更新⼀份数据,另⼀个事务在此时读取了同⼀份数据,由于某些原因,前⼀个RollBack了操作,则后⼀个事务所读取的数据就会是不正确的。

  6. 不可重复读(Non-repeatable read):在⼀个事务的两次查询之中数据不⼀致,这可能是两次查询过程中间插入了⼀个事务更新的原有的数据。
  7. 幻读(Phantom Read):在⼀个事务的两次查询中数据笔数不⼀致,例如有⼀个事务查询了几列(Row)数据,而另⼀个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

    8、ACID靠什么保证的?

    A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
    C⼀致性由其他三大特性保证、程序代码要保证业务上的⼀致性
    I隔离性由MVCC来保证
    D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可 以从redo log恢复

InnoDB redo log 写盘,InnoDB 事务进⼊ prepare 状态。
如果前⾯ prepare 成功,binlog 写盘,再继续将事务⽇志持久化到 binlog,如果持久化成功,那么 InnoDB 事务则进⼊ commit 状态(在 redo log ⾥⾯写⼀个 commit 记录)

redolog的刷盘会在系统空闲时进行

9、什么是MVCC

多版本并发控制:读取数据时通过⼀种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自已特定版本的数据,版本链
MVCC只在READ COMMITTED 和REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE 则会对所有读取的行都加锁。

聚簇索引记录中有两个必要的隐藏列:
trx_id:用来存储每次对某条聚簇索引记录进行修改的时候的事务id。
roll_pointer:每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中 。这个 roll_pointer就是存了⼀个指针,它指向这条聚簇索引记录的上⼀个版本的位置,通过它来获得上⼀个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)

已提交读和可重复读的区别就在于它们生成ReadView的策略不同。
开始事务时创建readview,readView维护当前活动的事务id,即未提交的事务id,排序生成⼀个数组
访问数据,获取数据中的事务id ( 获取的是事务id最大的记录) ,对比readview:
如果在readview的左边 ( 比readview都小) ,可以访问 ( 在左边意味着该事务已经提交)
如果在readview的右边 ( 比readview都大) 或者就在readview中,不可以访问,获取roll_pointer,取 上⼀版本重新对比 ( 在右边意味着,该事务在readview生成之后出现,在readview中意味着该事务还未 提交)
已提交读隔离级别下的事务在每次查询的开始都会生成⼀个独立的ReadView,而可重复读隔离级别则在 第⼀次读的时候生成⼀个ReadView,之后的读都复用之前的ReadView。
这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读 。通过ReadView生成策略的不 同实现不同的隔离级别。

10、分表后非sharding_key的查询怎么处理,分表后的排序?

  1. 可以做⼀个mapping表,比如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?所以我们可以做⼀个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过user_id去查询。
  2. 宽表,对数据实时性要求不是很高的场景,比如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成⼀张宽表,再基于其他如es提供查询服务。
  3. 数据量不是很大的话,比如后台的⼀些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做。或者异步的形式也是可以的。

union:排序字段是唯⼀索引:
首先第⼀页的查询:将各表的结果集进行合并,然后再次排序
第二页及以后的查询,需要传入上⼀页排序字段的最后⼀个值,及排序方式。
根据排序方式,及这个值进行查询。如排序字段date,上⼀页最后值为3,排序方式降序 。查询的 时候sql为select … from table where date < 3 order by date desc limit 0,10 。这样再将几个表的结果合并排序即可。

11、Mysql主从同步原理

mysql主从同步的过程:
Mysql的主从复制中主要有三个线程:master ( binlog dump thread) 、slave ( I/O thread 、SQL thread) ,Master⼀条线程和Slave中的两条线程。

  1. 主节点 binlog,主从复制的基础是主库记录数据库的所有变更记录到 binlog 。binlog是数据库服务器启动的那⼀刻起,保存所有修改数据库结构或内容的⼀个文件。
  2. 主节点 log dump 线程,当binlog 有变动时,log dump 线程读取其内容并发送给从节点。
  3. 从节点I/O线程接收binlog 内容,并将其写入到relay log 文件中。
  4. 从节点的SQL 线程读取relay log 文件内容对数据更新进行重放,最终保证主从数据库的⼀致性。

注:主从节点使用binglog 文件 + position 偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机重启,则会自动从position 的位置发起同步。
由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生⼀个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个 概念

全同步复制:主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。

半同步复制:和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至 少⼀个从库的确认就认为写操作完成。

12、简述MyISAM和InnoDB的区别

MyISAM:

  1. 不支持事务,但是每次查询都是原子的;
  2. 支持表级锁,即每次操作是对整个表加锁;
  3. 存储表的总行数;
  4. ⼀个MYISAM表有三个文件:索引文件、表结构文件、数据文件;
  5. 采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本⼀致,但是辅索引不用保证唯⼀性。

InnoDb:

  1. 支持ACID的事务,支持事务的四种隔离级别;
  2. 支持行级锁及外键约束:因此可以支持写并发;
  3. 不存储总行数;
  4. ⼀个InnoDb引擎存储在⼀个文件空间(共享表空间,表大小不受操作系统控制,⼀个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,⼀般为 2G),受操作系统文件大小的限制;
  5. 主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。

    13、简述Mysql中索引类型及对数据库的性能的影响

    普通索引:允许被索引的数据列包含重复的值。
    唯⼀索引:可以保证数据记录的唯⼀性。
    主键:是⼀种特殊的唯⼀索引,在⼀张表中只能定义⼀个主键索引,主键用于唯⼀标识⼀条记录,使用 关键字PRIMARY KEY 来创建。
    联合索引:索引可以覆盖多个数据列,如像INDEX(columnA, columnB)索引。
    全文索引:通过建立倒排索引 ,可以极大的提升检索效率,解决判断字段是否包含的问题,是目前搜索引擎使用的⼀种关键技术。可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引

索引可以极大的提高数据的查询速度。
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
但是会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件
索引需要占物理空间,除了数据表占数据空间之外,每⼀个索引还要占⼀定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大,如果非聚集索引很多,⼀旦聚集索引改变,那么所有非聚集索引都会跟着变。

14、Explain语句结果中各个字段分表表示什么

列名 描述
id 查询语句中每出现⼀个SELECT关键字,MySQL 就会为它分配⼀个唯⼀的id值,某些子查询会被 优化为join查询,那么出现的id会⼀样
select_type SELECT关键字对应的那个查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的查询方式 ( 全表扫描 、索引)
possible_keys 可能用到的索引
key 实际上使用的索引
key_len 实际使用到的索引长度
ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要读取的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分 比
Extra ⼀些额外的信息,比如排序等

15、索引覆盖是什么

索引覆盖就是⼀个SQL在执行时,可以利用索引来快速查找,并且此SQL所要查询的字段在当前索引对 应的字段中都包含了,那么就表示此SQL⾛完索引后不用回表了,所需要的字段都在当前索引的叶子节点上存在,可以直接作为结果返回了

16、最左前缀原则是什么

当⼀个SQL想要利用索引是,就⼀定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了⼀个联合索引,那么在写⼀个sql时就⼀定要提供a字段的条件,这 样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往右去比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则

17、Innodb是如何实现事务的

Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以⼀个update语句为例:

  1. Innodb在收到⼀个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool 中
  2. 执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
  3. 针对update语句生成⼀个RedoLog对象,并存入LogBuffer中
  4. 针对update语句生成undolog日志,用于事务回滚
  5. 如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中
  6. 如果事务回滚,则利用undolog日志进行回滚

    18、B树和B+树的区别,为什么Mysql使用B+树

    B树的特点:

  7. 节点排序

  8. ⼀个节点了可以存多个元素,多个元素也排序了

B+树的特点:

  1. 拥有B树的特点
  2. 叶子节点之间有指针
  3. 非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序

Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过⼀个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中⼀ 个Innodb页就是⼀个B+树节点,⼀个Innodb页默认16kb,所以⼀般情况下⼀颗两层的B+树可以存2000 万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。

19、Mysql锁有哪些,如何理解

按锁粒度分类:

  1. 行锁:锁某行数据,锁粒度最小,并发度高
  2. 表锁:锁整张表,锁粒度最大,并发度低
  3. 间隙锁:锁的是⼀个区间

还可以分为:

  1. 共享锁:也就是读锁,⼀个事务给某行数据加了读锁,其他事务也可以读,但是不能写
  2. 排它锁:也就是写锁,⼀个事务给某行数据加了写锁,其他事务不能读,也不能写

还可以分为:

  1. 乐观锁:并不会真正的去锁某行记录,而是通过⼀个版本号来实现的
  2. 悲观锁:上面所的行锁、表锁等都是悲观锁

在事务的隔离级别实现中,就需要利用锁来解决幻读

20、Mysql慢查询该如何优化?

  1. 检查是否⾛了索引,如果没有则优化SQL利用索引
  2. 检查所利用的索引,是否是最优索引
  3. 检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
  4. 检查表中数据是否过多,是否应该进行分库分表了
  5. 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源

    五、Redis(14)

    1、什么是RDB和AOF

    RDB:Redis DataBase,在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork⼀ 个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
    优点:

  6. 整个Redis数据库将只包含⼀个文件dump.rdb,方便持久化。

  7. 容灾性好,方便备份。
  8. 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化 。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了Redis的高性能
  9. 相对于数据集大时,比 AOF 的启动效率更高 。

缺点:

  1. 数据安全性低。RDB 是间隔⼀段时间进行持久化,如果持久化之间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
  2. 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF:Append Only File,以日志的形式记录服务器所处理的每⼀个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录

优点:

  1. 数据安全,Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是⼀旦系统出现宕机现象,那么这⼀秒钟之内修改的 数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。
  2. 通过append模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过Redis- check-aof 工具解决数据⼀致性问题。
  3. AOF机制的rewrite 模式。定期对AOF文件进行重写,以达到压缩的目的

缺点:

  1. AOF 文件比RDB 文件大,且恢复速度慢。
  2. 数据集大的时候,比rdb 启动效率低。
  3. 运行效率没有RDB高

AOF文件比RDB更新频率高,优先使用AOF还原数据,AOF比RDB更安全也更大,RDB性能比AOF好,如果两个都配了优先加载AOF。

2、Redis的过期键的删除策略

Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当 Redis中缓存的key过期了,Redis如何处理。
惰性过期:只有当访问⼀个key时,才会判断该key是否已过期,过期则清除 。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每隔⼀定的时间,会扫描⼀定数量的数据库的expires字典中⼀定数量的key,并清除其中已过期的key 。该策略是⼀个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的 指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键 。
Redis中同时使用了惰性过期和定期过期两种过期策略。

3、Redis线程模型、单线程快的原因

Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器file event handler 。 这个文件事件处理器,它是单线程的,所以Redis才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis 内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个Socket 、IO多路复用程序 、文件事件分派器以及事件处理器 ( 命令请求处理器 、命令回复处理器 、连接应答处理器等) 。
多个Socket 可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听 多个Socket,会将Socket 放入⼀个队列中排队,每次从队列中取出⼀个 Socket 给事件分派器,事件分派器把Socket 给对应的事件处理器。
然后⼀个Socket 的事件处理完之后,IO多路复用程序才会将队列中的下⼀个Socket 给事件分派器。文件事件分派器会根据每个Socket 当前产生的事件,来选择对应的事件处理器来处理。

单线程快的原因:
1) 纯内存操作
2) 核心是基于非阻塞的IO多路复用机制
3) 单线程反而避免了多线程的频繁上下文切换带来的性能问题

4、简述Redis事务实现

1、事务开始
MULTI命令的执行,标识着⼀个事务的开始。MULTI命令会将客户端状态的flags 属性中打开 Redis_MULTI 标识来完成的。
2、命令入队
当⼀个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客 户端发送的命令为MULTI、EXEC、WATCH、DISCARD中的⼀个,立即执行这个命令,否则将命令放 入⼀个事务队列里面,然后向客户端返回QUEUED回复
如果客户端发送的命令为EXEC 、DISCARD 、WATCH 、MULTI 四个命令的其中⼀个,那么服务器立即执行这个命令。
如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。
首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态( RedisClient) 的 flags 属性关闭Redis_MULTI 标识,并且返回错误信息给客户端。
如果正确,将这个命令放入⼀个事务队列里面,然后向客户端返回 QUEUED 回复

事务队列是按照FIFO的方式保存入队的命令

3 、事务执行
客户端发送EXEC 命令,服务器执行EXEC 命令逻辑。
如果客户端状态的flags 属性不包含Redis_MULTI 标识,或者包含Redis_DIRTY_CAS 或者Redis_DIRTY_EXEC 标识,那么就直接取消事务的执行。
否则客户端处于事务状态(flags 有Redis_MULTI 标识) ,服务器会遍历客户端的事务队列,然 后执行事务队列中的所有命令,最后将返回结果全部返回给客户端;

Redis 不支持事务回滚机制,但是它会检查每⼀个事务中的命令是否错误。
Redis 事务不支持检查那些程序员自已逻辑错误。例如对String 类型的数据库键执行对HashMap 类型的操作!

  1. WATCH 命令是⼀个乐观锁,可以为Redis 事务提供check-and-set(CAS)行为 。可以监控⼀个或多个键,⼀旦其中有⼀个键被修改 ( 或删除) ,之后的事务就不会执行,监控⼀直持续到EXEC命令
  2. MULTI命令用于开启⼀个事务,它总是返回OK 。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到⼀个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  3. EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
  4. 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退 出。
  5. UNWATCH命令可以取消watch对所有key的监控。

    5、Redis主从复制的核心原理

    通过执行slaveof命令或设置slaveof选项,让⼀个服务器去复制另⼀个服务器的数据。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库⼀般是只读的,并接受主数据库同步过来的数据。⼀个主数据库可以拥有多个从数据库,而⼀个从数据库只能拥有⼀个主数据库

全量复制:

  1. 主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU 、内存(页表复制) 、硬 盘IO的
  2. 主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗
  3. 从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行 bgrewriteaof,也会带来额外的消耗

部分复制:

  1. 复制偏移量:执行复制的双方,主从节点,分别会维护⼀个复制偏移量offset
  2. 复制积压缓冲区:主节点内部维护了⼀个固定长度的、先进先出(FIFO)队列作为复制积压缓冲区,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
  3. 服务器运行ID(runid):每个Redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自已的运行ID发送给从节点,从节点会将主节点的运行ID存起来。 从节点Redis断开重连的时候,就是根据运行ID来判断同步的进度:

    1. 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
    2. 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

      6、Redis有哪些数据结构?分别有哪些典型的应用场景?

      Redis的数据结构有:
  4. 字符串:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器 、Session共享、分布式 ID

  5. 哈希表:可以用来存储⼀些key-value对,更适合用来存储对象
  6. 列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据
  7. 集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能
  8. 有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能

    7、Redis分布式锁底层是如何实现的?

  9. 首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁

  10. 然后还要利用lua脚本来保证多个Redis操作的原子性
  11. 同时还要考虑到锁过期,所以需要额外的⼀个看门狗定时任务来监听锁是否需要续约
  12. 同时还要考虑到Redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个Redis节点挂掉了,锁也不能被其他客户端获取到

    8、Redis主从复制的核心原理

    Redis的主从复制是提高Redis的可靠性的有效措施,主从复制的流程如下:

  13. 集群启动时,主从库间会先建立连接,为全量复制做准备

  14. 主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照RDB
  15. 在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则,Redis的服务就被中断 了。但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中 。为了保证主从库的数据⼀致性,主 库会在内存中用专门的replication buffer ,记录RDB文件生成收到的所有写操作。
  16. 最后 ,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时replocation buffer中修改操作发送给从库,从库再执行这些 操作。这样⼀来,主从库就实现同步了
  17. 后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后,还会将写操 作发送给从库,实现增量同步

    9、Redis集群策略

    Redis提供了三种集群策略:

  18. 主从模式:这种模式比较简单,主库可以读写,并且会和从库进行数据同步,这种模式下,客户端直接连主库或某个从库,但是但主库或从库宕机后,客户端需要手动修改IP,另外,这种模式也比较难进行扩容,整个集群所能存储的数据受到某台机器的内存容量,所以不可能支持特大数据量

  19. 哨兵模式:这种模式在主从的基础上新增了哨兵节点,但主库节点宕机后,哨兵会发现主库节点宕机,然后在从库中选择⼀个库作为进的主库,另外哨兵也可以做集群,从而可以保证但某⼀个哨兵节点宕机后,还有其他哨兵节点可以继续工作,这种模式可以比较好的保证Redis集群的高可用,但是仍然不能很好的解决Redis的容量上限问题。
  20. Cluster模式:Cluster模式是用得比较多的模式,它支持多主多从,这种模式会按照key进行槽位的分配,可以使得不同的key分散到不同的主节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自已的多个从节点,如果该主节点宕机,会从它的从节点中选举⼀个新的主节点。

对于这三种模式,如果Redis要存的数据量不大,可以选择哨兵模式,如果Redis要存的数据量大,并且需要持续的扩容,那么选择Cluster模式。

10、缓存穿透、缓存击穿、缓存雪崩分别是什么

缓存中存放的大多都是热点数据,目的就是防止请求可以直接从缓存中获取到数据,而不用访问 Mysql。

  1. 缓存雪崩:如果缓存中某⼀时刻大批热点数据同时过期,那么就可能导致大量请求直接访问Mysql了,解决办法就是在过期时间上增加⼀点随机值,另外如果搭建⼀个高可用的Redis集群也是防止缓存雪崩的有效手段
  2. 缓存击穿:和缓存雪崩类似,缓存雪崩是大批热点数据失效,而缓存击穿是指某⼀个热点key突然失效,也导致了大量请求直接访问Mysql数据库,这就是缓存击穿,解决方案就是考虑这个热点key不设过期时间
  3. 缓存穿透:假如某⼀时刻访问Redis的大量key都在Redis中不存在(比如黑客故意伪造⼀些乱七八糟的key),那么也会给数据造成压力,这就是缓存穿透,解决方案是使用布隆过滤器,它的作用就是如果它认为⼀个key不存在,那么这个key就肯定不存在,所以可以在缓存之前加⼀层布隆过滤器来拦截不存在的key

    11、Redis和Mysql如何保证数据⼀致

  4. 先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不⼀致

  5. 先删除Redis缓存数据,再更新Mysql,再次查询的时候在将数据添加到缓存中,这种方案能解决1方案的问题,但是在高并发下性能较低,而且仍然会出现数据不⼀致的问题,比如线程1删除了Redis缓存数据,正在更新Mysql,此时另外⼀个查询再查询,那么就会把Mysql中老数据又查到Redis中
  6. 延时双删,步骤是:先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,这样就算在更新Mysql时,有其他线程读了Mysql,把老数据读到了Redis中,那么也会被删除掉,从而把数据保持⼀致

    12、Redis的持久化机制

    RDB:Redis DataBase 将某⼀个时刻的内存快照 ( Snapshot) ,以二进制的方式写入磁盘。 手动触发:

  7. save命令,使Redis 处于阻塞状态,直到 RDB 持久化完成,才会响应其他客户端发来的命令,所以在生产环境⼀定要慎用

  8. bgsave命令,fork出⼀个子进程执行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了
  9. 自动触发:
  10. save m n:在 m 秒内,如果有 n 个键发生改变,则自动触发持久化,通过bgsave执行,如果设置多个、只要满足其⼀就会触发,配置文件有默认配置(可以注释掉)
  11. flushall:用于清空Redis所有的数据库,flushdb清空当前Redis所在库数据(默认是0号数据库),会 清空RDB文件,同时也会生成dump.rdb 、内容为空
  12. 主从同步:全量同步时会自动触发bgsave命令,生成rdb发送给从节点

优点:

  1. 整个Redis数据库将只包含⼀个文件dump.rdb,方便持久化。
  2. 容灾性好,方便备份。
  3. 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化 。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了Redis的高性能
  4. 相对于数据集大时,比 AOF的启动效率更高 。

缺点:

  1. 数据安全性低。RDB 是间隔⼀段时间进行持久化,如果持久化之间 Redis 发生故障,会发生数据丢 失。所以这种方式更适合数据要求不严谨的时候
  2. 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟 。会占用cpu

AOF:Append Only File 以日志的形式记录服务器所处理的每⼀个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调操作系统命令进程刷盘

  1. 所有的写命令会追加到AOF 缓冲中。
  2. AOF 缓冲区根据对应的策略向硬盘进行同步操作。
  3. 随着AOF 文件越来越大,需要定期对AOF 文件进行重写,达到压缩的目的。
  4. 当Redis 重启时,可以加载 AOF 文件进行数据恢复 。
    1. 同步策略:每秒同步:异步完成,效率非常高,⼀旦系统出现宕机现象,那么这⼀秒钟之内修改的数据将会丢 失 每修改同步:同步持久化,每次发生的数据变化都会被立即记录到磁盘中,最多丢⼀条不同步:由操作 系统控制,可能丢失较多数据

优点:

  1. 数据安全
  2. 通过append 模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过Redis- check-aof 工具解决数据⼀致性问题。
  3. AOF 机制的rewrite 模式。定期对AOF文件进行重写,以达到压缩的目的

缺点:

  1. AOF 文件比RDB 文件大,且恢复速度慢。
  2. 数据集大的时候,比rdb 启动效率低。
  3. 运行效率没有RDB高

对比:

  1. AOF文件比RDB更新频率高,优先使用AOF还原数据。AOF比RDB更安全也更大
  2. RDB性能比AOF好
  3. 如果两个都配了优先加载AOF

    13、Redis单线程为什么这么快

    Redis基于Reactor模式开发了网络事件处理器、文件事件处理器 fileeventhandler 。它是单线程的,所 以Redis才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性。
    文件事件处理器的结构包含4个部分:多个Socket 、IO多路复用程序 、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)
    多个Socket 可能并发的产生不同的事件,IO多路复用程序会监听多个Socket,会将Socket 放入⼀个队列中排队,每次从队列中有序、同步取出⼀个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。
    然后⼀个Socket 的事件处理完之后,IO多路复用程序才会将队列中的下⼀个Socket 给事件分派器。文件事件分派器会根据每个Socket 当前产生的事件,来选择对应的事件处理器来处理。

  4. Redis启动初始化时,将连接应答处理器跟AE_READABLE事件关联。

  5. 若⼀个客户端发起连接,会产生⼀个AE_READABLE事件,然后由连接应答处理器负责和客户端建立 连接,创建客户端对应的socket,同时将这个socket的AE_READABLE事件和命令请求处理 器关联,使得客户端可以向主服务器发送命令请求。
  6. 当客户端向Redis发请求时 ( 不管读还是写请求) ,客户端socket都会产生⼀个AE_READABLE事件,触发命令请求处理器。处理器读取客户端的命令内容,然后传给相关程序执行。
  7. 当Redis服务器准备好给客户端的响应数据后,会将socket的AE_WRITABLE事件和命令回复处理 器关联,当客户端准备好读取响应数据时,会在socket产生⼀个AE_WRITABLE事件,由对应命令回复处理器处理,即将准备好的响应数据写入socket,供客户端读取。
  8. 命令回复处理器全部写完到socket 后,就会删除该socket的AE_WRITABLE事件和命令回复处理器的映射。

单线程快的原因:

  1. 纯内存操作
  2. 核心是基于非阻塞的IO多路复用机制
  3. 单线程反而避免了多线程的频繁上下文切换带来的性能问题

    14、简述Redis事务实现

  4. 事务开始:MULTI命令的执行,标识着⼀个事务的开始 。MULTI命令会将客户端状态的flags属性中打开Redis_MULTI标识来完成的。

  5. 命令入队:当⼀个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客 户端发送的命令为MULTI、EXEC、WATCH、DISCARD中的⼀个,立即执行这个命令,否则将命令放入⼀个事务队列里面,然后向客户端返回QUEUED回复,如果客户端发送的 命令为EXEC、DISCARD、WATCH、MULTI 四个命令的其中⼀个,那么服务器立即执行这个命令。如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(RedisClient)的 flags 属性关闭Redis_MULTI 标识,并且返回错误信息给客户端。如果正确,将这个命令放入⼀个事务队列里面,然后向客户端返回QUEUED回复事务队列是按照FIFO的方式保存入队的命令
  6. 事务执行:客户端发送EXEC 命令,服务器执行EXEC命令逻辑。如果客户端状态的flags属性不包含Redis_MULTI 标识,或者包含Redis_DIRTY_CAS 或者Redis_DIRTY_EXEC 标识,那么就直接取消事务的执行。否则客户端处于事务状态(flags有Redis_MULTI 标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端;Redis不支持事务回滚机制,但是它会检查每⼀个事务中的命令是否错误。Redis事务不支持检查那 些程序员自已逻辑错误。例如对String类型的数据库键执行对HashMap 类型的操作!

分布式与微服务(46)

image.png
image.png

image.png
image.png