熬夜总结的2022java面试题

java面试宝典

前言

面试不但要有工作经验,还得会点八股文,不然别人问起来,答不上来也是异常的尴尬,特别是应届毕业生,也是需要背点八股文,注重基础知识,才能在面试中得心应手,以下是我收集的面试题,以及我个人遇到的一些问题,做一些总结希望能帮助到大家。打了 * 号的都是我个人真实遇到过的。 答案只做参考,不喜勿喷,因为什么问题都没有标准的答案,只有你真正的明白了这个问题,你才能毫不费劲给别说出来。

java基础

什么是面向对象?

面向对象是模型化的,你只需抽象出一个类,这是一个抽象模板,在这里你拥有数据也拥有解决问题的方法。我们只需要操作这个对象,就可以使用里面的数据和方法。

面向对象的三大特征:封装,继承,多态

封装:在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部隐藏,外部无需修改或者关心内部实现。对内部数据进行保护。

继承:继承父类的方法,子类和父类公用一些方法和属性,可以达到功能的扩展和代码复用。

多态:基于对象所属类不同,外部对同一个方法的调用,实际执行逻辑不同。继承特点:方法重写,父类引用指向子类对象,编译看左边,运行看右边

值传递和引用传递?*

Java中都是值传递

参数类型

  • 形参:方法被调用时需要传递进来的参数,如:func(int a)中的a,它只有在func被调用期间a才有意义,也就是会被分配内存空间,在方法fun执行完成后, a 就会被销毁释放空间,也就是不存在了

  • 实参:方法被调用时是传入的实际值,它在方法被调用前就已经被初始化并旦在方法被调用时传入。

值传递与引用传递

  • 值传递:在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。

  • 引用传递:"引用"也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向同一块内存地址,对形参的操作会影响的真实内容。(java中只有值传递)

Java中都是值传递

Object类型,除了这种不可变的类以外,你传到另外一个方法中去修改其里面的属性时,原有的对象中的值也会发生,原因是形参拷贝了实参的地址作为副本,他们俩共用一个地址,所以会改掉堆中的那个对象中的属性值 (叫传递引用)

传递值

当我们用这种基本数据类型、不可变类型( String, Integer,Long)之类的做形参, 他们的实参值不会改变

==和equals的区别是什么?

什么是==?

== 等于比较运算符,如果进行比较的两个操作数都是数值类型,即使他们的数据类型不相同,只要他们的值相等,也都将返回true.如果两个操作数都是引用类型,那么只有当两个引用变量的类型具有父子关系时才可以比较,而且这两个引用必须指向同一个对象,才会返回true.(在这里我们可以理解成==比较的是两个变量的内存地址)

什么是equals()?

equals()方法是Object类的方法,在Object类中的equals()方法体内实际上返回的就是使用==进行比较的结果.但是我们知道所有的类都继承Object,而且Object中的equals()方法没有使用final关键字修饰,那么当我们使用equals()方法进行比较的时候,我们需要关注的就是这个类有没有重写Object中的equals()方法. 如果重写了equals方法就按照自己的重写的规则进行比较,如果没有重写就继承自Object类的equals方法进行比较内存地址(只有引用类型的对象才可以调用equals方法)。

重载和重写的区别?

1.重写(Override)

重写就是重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。 方法重写又叫方法覆盖。

重写总结:
1.发生在父类与子类之间
2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
5.父类的静态方法是不能被重写的。

2.重载(Overload)

在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载

重载总结:
1.重载Overload是一个类中多态性的一种表现
2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准

区别总结:

1、重写实现的是运行时的多态,而重载实现的是编译时的多态。

2、重写的方法参数列表必须相同;而重载的方法参数列表必须不同。

3、重写的方法的返回值类型只能是父类类型或者父类类型的子类,而重载的方法对返回值类型没有要求。

4、重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常,重载不做限制

5、重写对访问修饰符的限制一定要大于等于被重写方法的访问修饰符,重载不做限制

抽象类和接口的区别 *

类型abstract classInterface
定义abstract class关键字Interface关键字
继承抽象类只可以继承一个类接口可以继承接口(一个或多个接口)
访问修饰符抽象方法可以有publicprotecteddefault这些修饰符接口方法默认修饰符是public。你不可以使用其它修饰符
方法实现可定义构造方法,可以有抽象方法和具体方法,可以没有抽象方法,但是抽象方法只能再抽象类中接口完全是抽象的,没构造方法,且方法都是抽象的,不存在方法的实现jdk8后有默认实现
作用抽象类是对一种事物的抽象接口是对行为的抽象
成员变量抽象类中可以有成员变量public static final修饰的常量

抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类行为进行抽象。

构造器 Constructor 是否可被 override?

构造器不能被@Override(重写),构造器只能被overload(重载)。

java静态变量、代码块、和静态方法的执行顺序是什么?

基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块

代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块

继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器

想要深入了解,可以参考这篇文章 :https://juejin.cn/post/6844903986475040781

break ,continue ,return 的区别及作用?

  • break 跳出当前包含break的循环,不再执行循环(结束当前的循环体)
  • continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
  • return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)

final和finally以及finalize区别?

final修饰符:可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、
修饰变量表示该变量是一个常量不能被重新赋值;

finally代码块中:一般作用在try-catch代码块中,在处理异常时通常将一定要执行的代码方法放在finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。

注:有些情况不会执行finally

  • 只有与finally对应的try语句块得到执行的情况下,finally语句块才会执行。如果在执行try语句块之前已经返回或抛出异常,那么try对应的finally语句并没有执行
  • 我们在try语句块中执行了System.exit (0) 语句,终止了Java虚拟机的运行;
  • 如果在try-catch-finally语句中执行return语句,finally语句在该代码中一定会执行,因为finally用法特殊会撤销之前的return语句,继续执行最后的finally块中的代码;

finalize一个方法:属于所有类的父类Object类的一个方法,也就是说每一个对象都有这么个方法;Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作;调用super.finalize();

这个方法在GC启动该对象被回收的时候被调用。GC可以回收大部分的对象(凡是new出来的对象GC都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。
特殊情况下,需要程序员实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。

谈谈你对多态的理解 *

同一个对象,在不同时刻体现出来的不同状态

  • 多态的关键是每个子类都要重写方法,实现了继承了同样的方法名称但是又有每个的特点,就像龙生九子,每个不一样,有两个好处,一个是类关系清晰,另一个是方法调用方便,只要有传入实参就行。

  • 多态是Java面向对象三个特征中的一个也是做主要的一个,所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
    多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。

多态的实现

Java实现多态有三个必要条件:继承、重写、向上转型。

  • 继承:在多态中必须存在有继承关系的子类和父类(实现关系接口)。
  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
  • 向上转型:在多态中需要将父类或者父接口引用指向子类Fu f= new Zi(),只有这样该引用才能够具备技能调用父类的方法和子类的方法。

只有满足了这三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。

向上转型

父类对象通过子类对象去实例化,实际上就是对象的向上转型。向上转型是不需要进行强制类型转换的,但是向上转型会丢失精度。

向下转型

所谓向下转型,也就是说父类的对象可以转换为子类对象,但是需要注意的是,这时则必须要进行强制的类型转换。

多态的好处

  • 可替换性:多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
  • 可扩充性:多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
  • 接口性:多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。超类Shape规定了两个实现多态的接口方法,computeArea()以及computeVolume()。子类,如Circle和Sphere为了实现多态,完善或者覆盖这两个接口方法。
  • 灵活性:它在应用中体现了灵活多样的操作,提高了使用效率。
  • 简化性:多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

对象的序列化和反序列化

  • Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化deserialization是一种将这些字节重建成一个对象的过程;

  • 序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,就使得数据能够被轻松地存储和传输。

  • Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程;

1.序列化是干什么的?

  • 简单说就是为了保存在内存中的各种对象的状态,并且可以把保存的对象状态再读出来。虽然你可以用自己的各种方法来保存Object states, 但是Java给你提供一种应该比你自己好的保存对象状态的机制、那就是序列化。

2.什么情况下需要序列化?

  • 当你想把的内存中的对象保存到一个文件或者数据库中时候;
  • 当你想用套接字在网络上传送对象的时候;
  • 当你想通过RMI传输对象的时候(RMI->Remote Method Invocation 远程方法调用)

简述java继承

继承可以降低代码编写的冗余度,提高编程的效率。通过继承,子类获得了父类的成员变量和方法。

**继承的作用:**通过继承可以快速创建新的类,实现代码的重用,提高程序的可维护性,节省大量创建新类的时间,提高开发效率和开发质量。

Java不支持多重继承,但一个类可以实现多个接口,从而克服单继承的缺点;

构造方法不会被子类继承,但可以从子类中调用父类的构造方法。

子类变量访问顺序:先找局部变量,局部变量就找当前类的成员变量,再找不到就找父类的成员变量

子类无法继承父类中私有的内容(可以继承但是没有访问权限)

可以在子类声明父类已有的方法和属性,从而隐藏父类的属性和方法

子类可以直接使用从父类继承过来的属性和方法,可以使用super关键字调用,也可以隐式调用

继承的优点

1.继承过来的字段和方法,可以像任何其他字段和方法一样被直接使用;
2.在子类中可以声明一个与父类中同名的新字段或静态方法,从而“隐藏”父类中的字段或方法;
3.可以在子类中声明一个在父类中没有的新字段和方法;
4.可以在子类中编写一个父类当中具有相同名的新实例方法,这称为“方法重写”或“方法覆盖”;
5.可以在子类中编写一个调用父类构造方法的子类构造方法,既可以隐式地实现,也可以通过使 用关键字super来实现。

重写父类方法

子类继承了父类中的所有成员及方法,但在某种情况下,子类中该方法所表示的行为与其父类中该方法所表示的行为不完全相同.

当一个子类中的一个实例方法具有与其父类中的一个实例方法相同的名称,相同的参数列表和返回值时,称子类中的方法“重写”了父类的方法。

隐藏父类中的方法

如果一个子类定义了一个静态类方法,而这个类方法与其父类的一个类方法具有相同的签名(指名称、参数格式和类型)和返回值,则称在子类中的这个类方法“隐藏”了父类中的该类方法。

使用super关键字

使用super调用父类中重写的方法、访问父类中被隐藏的字段

当使用无参数的super()时,父类的无参数构造方法就会被调用;

当使用带有参数的super()方法时,父类的有参数构造方法就会被调用。

super 可以调用父类的方法和成员变量

在子类创建对象时会默认调用父类的无参构造方法。

this访问本类的成员,super访问父类的成员

final关键字

final关键字可用于修饰类、变量和方法,它有“无法改变”或者“最终”的含义,因此被final修饰的类、变量和方法将具有以下特性:

final可以修饰类,方法,变量

final修饰的类不可以被继承

final修饰的方法不可以被覆盖

final修饰的变量是一个常量,只能被赋值一次

为什么要用final修饰变量,其实,在程序中如果一个数据是固定的。那么直接使用这个数据就可以了,但是这种阅读性差,所以应该给数据起个名称。而且这个变量名称的值不能变化,所以加上final固定写法规范:常量所有字母都大写,多个单词,中间用_连接。

JDK和JRE的区别?

  1. JDK 和 JRE 有什么区别?

    JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
    JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。

具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。

两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

不对,两个对象的 hashCode()相同,equals()不一定 true。

    public static void main(String[] args) {
        String str1 = "通话";
        String str2 = "重地";
        System.out.println(String.format("str1=%d ; str2=%d",  str1.hashCode(),str2.hashCode()));
        System.out.println(str1.equals(str2));
    }

很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。

什么是反射机制?

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

静态编译:在编译时确定类型,绑定对象

动态编译:运行时确定类型,绑定对象

反射机制优缺点

优点: 运行期类型的判断,动态加载类,提高代码灵活度;

缺点: 性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的Java代码要慢很多;

String 属于基础的数据类型吗?

不是,String是属于引用数据类型,也就是对象类型。

基本数据类型只有八种:byte、boolean、char、short、int、float、long、double

对应的包装类型:Byte、Short、Integer、Long、Float、Double、Boolean、Character

是否可以继承String类?

String类是final类,不可以被继承。

java 中操作字符串都有哪些类?它们之间有什么区别?

操作字符串的类有:String、StringBuffer、StringBuilder。

String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。

StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。

如何将字符串反转?

使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。

String 类的常用方法都有那些?

indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。

抽象类和普通类的区别?

普通类不能包含抽象方法,抽象类可以包含抽象方法。

抽象类不能直接实例化,普通类可以直接实例化。

java 中 IO 流分为几种? *

按功能来分:输入流(input)、输出流(output)。

按类型来分:字节流和字符流。

字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输,以字符为单位输入输出数据。

BIO、NIO、AIO 有什么区别?

①BIO是同步阻塞的,数据的读写会阻塞在一个线程中,适用于连接数目比较小且固定的架构,对服务器资源要求高,JDK1.4前的唯一选择。

②NIO是同步非阻塞的,通过Selector监听Channel上事件的变化,在Channel上有数据发生变化时通知该线程进行读写操作。适用于连接数目比较多且连接比较短的架构,如聊天服务器,从 JDK1.4开始支持。

③AIO是异步非阻塞的,异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情。适用于连接数目比较多且连接比较长的架构,从JDK1.7开始支持。

14.Files的常用方法都有哪些?

Files.exists():检测文件路径是否存在。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件。

java集合哪些?

①主要有两个接口Collection和Map,其中Collection又包括List、Set和Queue。

②List是有序的,主要包括ArrayList,LinkedList和Vector,ArrayList底层通过数组实现,线程不安全,Vector是线程安全的ArrayList,但效率较低,LinkedList底层通过双向链表实现,与ArrayList相比增删快查询慢。

③Set是唯一且无序的,主要包括HashSet,LinkedHashSet和TreeSet。HashSet底层其实就是HashMap,利用了key来保证元素的唯一性。LinkedHashSet可以按照key的操作顺序排序,TreeSet支持按照默认或指定的排序规则排序。

④Queue是队列结构,主要有ArrayBlockingQueue基于数组的阻塞队列、LinkedBlockingQueue基于链表的阻塞队列等。

⑤Map以key-value键值对的形式存储元素,主要包括HashMap、LinkedHashMap和TreeMap。HashMap底层通过数组+链表/红黑树实现,LinkedHashMap可以按照key的操作顺序对集合排序,TreeMap可以按照默认或指定的排序规则对集合排序。

在使用foreach循环遍历集合元素时能否添加或删除元素?

使用foreach循环遍历元素集合时不能修改或删除元素,通过java -c查看字节码可以发现foreach循环实际上是用Iterator迭代器实现的,如果进行添加或删除元素会抛出ConcurrentModificationException异常,因为添加或删除元素会改变modCount的值,modCount是集合类的一个成员变量,代表集合的修改次数,当modCount的值和预期的exceptedModCount值不一致时就会抛出ConcurrentModificationException异常。

Collection 和 Collections 有什么区别?

java.util.Collection 集合类的一个顶级接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

List、Set、Map 之间的区别是什么?

List:有序集合、元素可重复;ArrayList基于数组实现的有序集合;LinkedList基于链表实现的有序集合。可以用itertor取出所有元素,也可以根据索引取出元素

Set:无序集合、元素不可重复,最多只能允许有一个null;LinkedHashSet按照插入排序,SortSet可排序,HashSet无序。取出元素的时候只能Itertor取出所有元素在逐个遍历,不能根据索引取出元素

Map:键值对集合、储存键、值和之间的映射,Key无序,唯一,最多允许一个null;Value不要求有序,允许重复。

HashMap 和 Hashtable 有什么区别?

hashMap去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法。

hashTable线程安全的,HashMap是线程不安全的,HashMap的效率比HashTable要高

hashMap允许空键值,hashTable不允许空键值。

如何决定使用 HashMap 还是 TreeMap

对于在HashMap中插入、删除和定位元素这类操作,HashMap是最好的选择。假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将HashMap换为TreeMap进行有序key的遍历。TreeMap中的父接口中定义了comparator()方法,所以TreeMap在添加的时候key可以按照顺序进行添加。

HashMap实现原理?*

hashMap是一个key-value的键值对集合,key无序不可重复,key可以为null,但是只能允许有一个null,key如果重复value会被替换掉。HashMap1.8是由数组+链表+红黑树构成

  1. HashMap底层维护了Node类型的数组table,默认为null
  2. 当创建对象时,将加载因子(loadfactor)设置为默认值为0.75,可以通过构造器传入手动设置加载因子和初始容量
  3. 当往map中添加数据时,根据key计算出哈希索引值,得到在table中的下标位置,然后判断该索引位置是否有元素
    1. 如果没有,构建一个Node进行添加
    2. 如果该索引位置有元素,继续判断该元素的key和准备加入的元素的key是否相同,如果相同,则替换value
    3. 如果不相同,需要判断是数结构,还是链表还是红黑树,做出相应的处理,如果添加时,发现容量不够,则需要扩容。
  4. 第一次添加需要扩容table的容量为16,临界值为12 【0.75 x 16】临界值=加载因子*当前tableLength
  5. table以后再次扩容,就是原本容量的2倍进行扩容,如果链表中的元素超过了8个,并且table的大小超过64就会转换为红黑树

Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

说一下 HashSet 的实现原理?

HashSet:可以存放null值,但是只能允许有一个null,不保证元素顺序是有序的,取决于hash过后,得到的索引进行确定位置,不能有重复的元素,如果添加重复值,会被新的值替换掉。HashSet底层是HashMap,HashSet的value是HashMap的key,这个map的value部分是添加的是一个Object类型的常量值,因为在移除元素和添加元素的时候会根据返回值来判断是否成功。

思考为什么HashSet的底层的map对象的value部分为什么不放null?

答:我们都知道HashSet底层的使用的HashMap 的key作为HashSet的Value,但是为什么不放一个null呢?而是直接放一个常量类型的Object对象,原因是因为HashSet在调用remove()的时候调用的是map.remove(),HashSet.remove() 需要返回一个布尔值,而HashMap的remove的时候需要判断remove的元素是否为空来进行判断是否移除成功,如果放的都是null,那就不能进行判断是否移除成功了。

ArrayList 和 LinkedList 的区别是什么?

• 数据结构实现:
ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
• 随机访问效率:
ArrayList 比 LinkedList 在随机访问的时候效率要高,
因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
• 增加和删除效率:
在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,
因为 ArrayList 增删操作要影响数组内的其他数据的下标。
• 综合来说:
在需要频繁读取集合中的元素时,更推荐使用 ArrayList,
而在插入和删除操作较多时,更推荐使用 LinkedList。ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。

如何实现数组和 List 之间的转换?

List转换成为数组:调用ArrayList的toArray方法。

数组转换成为List:调用Arrays的asList方法。

ArrayList 和 Vector 的区别是什么?

同步性:
Vector是线程安全的,也就是说它的方法是线程同步的,而ArrayList是线程不安全的,它的方法之间是线程不同步的如果只有一个线程去访问集合那么使用ArrayList,他不考虑线程安全的问题,所以效率会高一些,如果是多个线程去访问集合,那么使用Vector。

数据增长性:

ArrayList和Vector集合都有一个初始容量的大小,当元素的个数超过存储容量是,就需要增加ArrayList和Vector的存储空间,每次增加不是增加一个而是增加多个,Vector是增加原来的两倍,从源码中可以看出ArrayList增长原来的1.5倍,ArrayList和Vector可以设置初始的存储空间的大小,Vector还以设置增长空间大小,而ArrayList不可以。

Array 和 ArrayList 有何区别?

Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小的,而ArrayList大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。

在 Queue 中 poll()和 remove()有什么区别?

poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。

迭代器 Iterator 是什么?

它是 Java 中常用的设计模式之一。用于顺序访问集合对象的元素,无需知道集合对象的底层实现。

Iterator 是可以遍历集合的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现,从而解耦。缺点是增加新的集合类需要对应增加新的迭代器类,迭代器类与集合类成对增加。迭代器通常被称为“轻量级”对象,因为创建它的代价小。

Iterator 怎么使用?有什么特点?

java中的Iterator功能比较简单,并且只能单向移动:

使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。

使用next()获得序列中的下一个元素。

使用hasNext()检查序列中是否还有元素。

使用remove()将迭代器新返回的元素删除。

Iterator 和 ListIterator 有什么区别?

Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引。

final的作用?

为什么局部内部类和匿名内部类只能访问局部的final修饰的变量?

因为在编译之后会产生两个class文件,有可能外部类执行完毕变量被回收了,但是里面的内部类还在引用这个变量,这就导致了内部类访问了一个不存在的变量,为了解决这个问题,那就将外部类的局部变量复制一份到内部类作为成员变量,即使外部类销毁,内部类也能正常使用。

final修饰类:表示该类不能被继承

final修饰变量:表示这个变量的值一旦赋值,不能在做修改,在使用之前一定要赋值

final修饰的是方法:不可以被子类重写,但是可以重载

final修饰的是静态变量:一定要为其赋值,要么就是在静态代码块中给他赋值,在使用之前一定要赋值

final修饰的是引用类型变量:该对象的引用不可以被改变,但是该对象中的属性是可以改变的

Java中的异常体系

Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常。RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。

HashCode和equals

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

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

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

ConcurrentHashMap原理

jdk7:
数据结构: ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
元素查询;二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
锁: Segment分段锁Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment
get方法无需加锁,volatile保证

jdk8:
数据结构: synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容

读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见数组用volatile修饰,保证扩容时被读线程感知

设计模式有哪些原则?

①单一职责原则:单一职责原则又称单一功能原则,它规定一个类只有一个职责。如果有多个职责(功能)设计在一个类中,这个类就违反了单一职责原则。

②开闭原则:开闭原则规定软件中的对象(类、模块、函数等)对扩展开放,对修改封闭,这意味着一个实体允许在不改变其源代码的前提下改变其行为,该特性在产品化的环境下是特别有价值的,在这种环境下,改变源代码需要经过代码审查,单元测试等过程以确保产品的使用质量。遵循这个原则的代码在扩展时并不发生改变,因此不需要经历上述过程。

③里氏代换原则:里氏代换原则是对开闭原则的补充,规定了在任意父类可以出现的地方,子类都一定可以出现。实现开闭原则的关键就是抽象化,父类与子类的继承关系就是抽象化的具体表现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

④依赖倒转原则:依赖倒转原则指程序要依赖于抽象(Java中的抽象类和接口),而不依赖于具体的实现(Java中的实现类)。简单地说,就是要求对抽象进行编程,不要求对实现进行编程,这就降低了用户与实现模块之间的耦合度。

⑤接口隔离原则:接口隔离原则是指通过将不同的功能定义在不同的接口中来实现接口的隔离,这样就避免了其他类在依赖该接口(接口上定义的功能)时依赖其不需要的接口,可减少接口之间依赖的冗余性和复杂性。

⑥合成/聚合复用原则:合成/聚合复用原则指通过在一个新的对象中引入(注入)已有的对象以达到类的功能复用和扩展的目的。它的设计原则是要尽量使用合成或聚合而不要使用继承来扩展类的功能。

⑦迪米特法则:迪米特法则指一个对象尽可能少地与其他对象发生相互作用,即一个对象对其他对象应该有尽可能少的了解或依赖。其核心思想在于降低模块之间的耦合度,提高模块的内聚性。迪米特法则规定每个模块对其它模块都要有尽可能少的了解和依赖,因此很容易使系统模块之间的功能独立,这使得各个模块的独立运行变得更加简单,同时使得各个模块之间的组合变得更加容易。

设计模式有哪些分类?

①创建型模式:提供了多种优雅创建对象的方法,包括工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式

②结构型模式:通过类和接口之间的继承和引用实现创建复杂结构对象的功能,包括适配器模式、桥接模式、组合模式、装饰器模式、外观模式、享元模式、代理模式

③行为型模式:通过类之间不同的通信方式实现不同的行为方式,包括责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板模式、访问者模式

1.简述工厂模式

①工厂模式是最常见的设计模式,该模式属于创建型模式,它提供了一种简单、快速、高效而安全地创建对象的方式。
②工厂模式在接口中定义了创建对象的方法,而将具体的创建对象的过程在子类中实现,用户只需通过接口创建需要的对象即可,不用关注对象的具体创建过程。同时,不同的子类可根据需求灵活实现创建对象的不同方法。
③通俗地讲,工厂模式的本质就是用工厂方法代替new操作创建一种实例化对象的方式,以便提供一种方便地创建有同种类型接口地产品的复杂对象。

2.简述抽象工厂模式

①抽象工厂模式在工厂模式上添加了一个创建不同工厂的抽象接口(抽象类或接口实现),该接口可叫做超级工厂。在使用过程中,我们首先通过抽象接口创建出不同的工厂对象,然后根据不同的工厂对象创建不同的对象。
②在同一个厂商有多个维度的产品时,如果使用工厂模式,则势必会存在多个独立的工厂,这样的话设计和物理世界是不对应的。正确的做法是通过抽象工厂模式来实现,我们可以将抽象工厂类比成厂商,将通过抽象工厂创建出来的工厂类比成不同产品的生产线,在需要生产产品时根据抽象工厂生产。

3.讲一讲单例模式

①单例模式是保证系统实例唯一性的重要手段。单例模式首先通过将类的实例化方法私有化来防止程序通过其他方式创建该类的实例,然后通过提供一个全局唯一获取该类实例的方法帮助用户获取类的实例,用户只需也只能通过调用该方法获取类的实例。
②单例模式的设计保证了一个类在整个系统中同一时刻只有一个实例存在,主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。同时单例模式为系统资源的优化提供了很好的思路,频繁创建或销毁对象都会增加系统的资源消耗,而单例模式保障了整个系统只有一个对象能被使用,很好地节约了资源。
③单例模式的实现很简单,每次在获取对象前都判断系统是否已经有这个单例对象,有则返回,无则创建。需要注意的是,单例模型的类构造器是私有的,只能由自身创建和销毁对象,不允许除了该类的其他程序使用new关键字创建对象及破坏单例模式。

4.懒汉模式线程安全吗?

①懒汉模式是线程不安全的,在需要时才会创建实例对象。
②可以通过加synchronized锁实现线程安全的懒汉模式。
③可以在加锁的基础上使用volatile关键字和双重校验锁进一步提升懒汉模式的线程安全性。

5.讲一讲建造者模式

①建造者模式使用多个简单的对象创建一个复杂的对象,用于将一个复杂的构建与其表示分离,使得同样的构建过程可以创建不同的表示,然后通过一个Builder类(该Builder类是独立于其他对象的)创建最终的对象。
②建造者模式主要用于解决软件系统中复杂对象的创建问题,比如有些复杂对象的创建需要通过各部分的子对象用一定的算法构成,在需求变化时这些复杂对象将面临很大的改变,不利于系统稳定。但是使用建造者模式能将它们各部分的算法包装起来,在需求变化后只需调整各个算法的组合方式和顺序,能极大提供系统稳定性。建造者模式常被用于一些基本部件不会变而其组合经常变化的应用场景下。
③建造者模式与工厂模式的最大区别是,建造者模式更关注产品的组合方式和装配顺序,而工厂模式关注产品的生产本身。
④建造者模式在设计时有以下几种角色:Builder 创建一个复杂产品对象的抽象接口、ConcreteBuilder Builder接口的实现类,用于定义复杂产品各个部件的装配流程、Director 构造一个使用Builder接口的对象、Product 表示被构造的复杂对象,ConcreteBuilder定义了该复杂对象的装配流程,而Product定义了该复杂对象的结构和内部表示。

6.讲一讲原型模式

①原型模式指通过调用原型实例的Clone方法或其他手段来创建对象。
②原型模式属于创建型设计模式,它以当前对象为原型来创建另一个新的对象,而无需知道创建的细节。原型模式在Java中通常使用Clone技术实现,在JavaScript中通常使用对象的原型属性实现。原型模式的Java实现很简单,只需要原型类实现Cloneable接口并重写clone方法即可。

7.讲一讲适配器模式

①适配器模式通过定义一个适配器类作为两个不兼容的接口之间的桥梁,将一个类的接口转换成用户期望的另一个接口,使得两个或多个原本不兼容的接口可以基于适配器类一起工作。
②适配器模式主要通过适配器类实现各个接口之间的兼容,该类通过依赖注入或者继承实现各个接口的功能并对外统一提供服务。在适配器模式的实现中有三种角色:source、targetable、adapter。sourc是待适配的类,targetable是目标接口,adapter是适配器。我们在具体应用中通过adapter将source的功能扩展到targetable,以实现接口的兼容。适配器的实现主要分为三类:类适配器模式、对象适配器模式、接口适配器模式。

8.讲一讲装饰者模式

①装饰者模式指在无需改变原有类及类的继承关系的情况下,动态扩展一个类的功能。它通过装饰者来包裹真实的对象,并动态地向对象添加或者撤销功能。②装饰者模式包括Source和Decorator两种角色,source是被装饰者,decorator是装饰者。装饰者模式通过装饰者可以为被装饰者Source动态地添加一些功能。

9.讲一讲代理模式

①代理模式指为对象提供一种通过代理的方式来访问并控制该对象行为的方法。在客户端不适合或者不能够直接引用一个对象时,可以通过该对象的代理对象实现对该对象的访问,可以将该代理对象理解为客户端和目标对象之间的中介者。
②在代理模式下有两种角色,一种是被代理者,一种是代理(Proxy),在被代理者需要做一项工作时,不用自己做而是交给代理做。以企业招聘为例,不用自己去市场找,可以通过代理去找。

10.讲一讲外观模式

①外观模式也叫做门面模式,通过一个门面向客户端提供一个访问系统的统一接口,客户端无需关心和知晓系统内部各子模块(系统)之间的复杂关系,其主要目的是降低访问拥有多个子系统的复杂系统的难度,简化客户端与其之间的接口。外观模式将子系统中的功能抽象成一个统一的接口,客户端通过这个接口访问系统,使得系统使用起来更加容易。
②简单来说外观模式就是将多个子系统及其之间的复杂关系和调用流程封装到一个统一的接口或类中以对外提供服务,这种模式设计三种角色:子系统角色:实现了子系统的功能;门面角色:外观模式的核心, 熟悉各子系统的功能和调用关系并根据客户端的需求封装统一的方法来对外提供服务;客户角色:通过调用门面来完成业务功能。

11.讲一讲桥接模式

①桥接模式通过将抽象及其实现解耦,使二者可以根据需求独立变化。这种类型的设计模式属于结构型模式,通过定义一个抽象和实现之间的桥接者来达到解耦的目的。
②桥接模型主要用于解决在需求多变的情况下使用继承造成类爆炸的问题,扩展起来不够灵活。可以通过桥接模式将抽象部分与实现部分分离,使其能够独立变化而相互之间的功能不受影响。具体的做法是通过定义一个桥接接口,使得实体类的功能独立于接口实现类,降低他们之间的耦合度。

12.讲一讲组合模式

①组合模式又叫做部分整体模式,主要用于实现部分和整体操作的一致性。组合模式常根据树形结构来表示部分及整体之间的关系,使得用户对单个对象和组合对象的操作具有一致性。
②组合模式通过特定的数据结构简化了部分和整体之间的关系,使得客户端可以像处理单个元素一样来处理整体的数据集,而无需关心单个元素和整体数据集之间的内部复杂结构。

13.讲一讲享元模式

①享元模式主要通过对象的复用减少对象创建的次数和数量,减少系统内存的使用和降低系统负载。享元模式属于结构型模型,在系统需要一个对象时享元模式首先在系统中查找并尝试重用现有的对象,如果未找到匹配对象则创建新对象并将其缓存在系统中。
②享元模式主要用于避免在有大量对象时频繁创建和销毁对象造成系统资源的浪费,把其中共同的部分抽象出来,如果有相同的业务请求则直接返回内存中已有的对象。

14.讲一讲策略模式

①策略模式为同一个行为定义了不同策略,为每种策略实现了不同方法。用户使用时系统根据不同的策略自动切换不同的方法实现策略的改变。同一策略下的不同方法是对同一功能的不同实现,因此在使用时可相互替换而不影响用户的使用。
②策略模式的实现是在接口中定义不同的策略,在实现类中完成了对不同策略下具体行为的实现,并将用户的策略状态存储在上下文中来完成策略的存储和状态的改变。

15.讲一讲模板方法模式

①模板方法模式定义了一个算法框架,并通过继承的方式将算法的实现延迟到子类中,使得子类可以在不改变算法框架及其流程的前提下重新定义该算法在某些特定环节的实现,是一种类行为型模式。
②该模式在抽象类中定义了算法的结构并实现了公共部分算法,在子类中实现可变的部分并根据不同的业务需求实现不同的扩展。模板方法模式的优点在于其父类(抽象类)中定义了算法的框架以及保障算法的稳定性,同时在父类中实现了算法公共部分的方法保证代码的复用,将部分算法延迟到子类实现,因此子类可以通过继承扩展或重新定义算法的功能而不影响稳定性,符合开闭原则。
③抽象类:定义算法框架,由基本方法和模板方法组成。基本方法定义了算法有哪些环节,模板方法定义了算法各个环节执行的流程。具体子类:对在抽象类中定义的算法根据需求进行不同的实现。

16.讲一讲观察者模式

①观察者模式指在被观察者的状态发生变化时,系统基于事件驱动理论将其状态通知到订阅其状态的观察者对象中,以完成状态的修改和事件传播。观察者模式是一种对象行为模式,观察者和被观察者之间的关系属于抽象耦合关系,主要优点是观察者与被观察者之间建立了一套事件触发机制,以降低二者之间的耦合度。
②观察者模式的主要角色如下:抽象主题Subject:持有订阅了该主题的观察者对象的集合,同时提供了增加删除观察者对象的方法和主题状态变化后的通知方法。具体主题Concrete Subject:实现了抽象主题的通知方法,在主题内部状态发生变化时,调用该方法通知订阅了主题状态的观察者对象。抽象观察者Observer:观察者的抽象类或接口,定义了主题状态变化时需要调用的方法。具体观察者 Concrete Observer:抽象观察者的实现类,在收到主题状态变化的信息后执行具体触发机制。

17.讲一讲迭代器模式

①迭代器模式提供了顺序访问集合对象中的各种元素,而不暴露该对象内部结构的方法。Java中的集合就是典型的迭代器模式,比如HashMap,当遍历HashMap时,需要迭代器不停地获取Next元素就可以循环遍历集合中所有元素。
②迭代器模式将遍历集合中所有元素地操作封装成一个迭代器类,目的是在不暴露集合对象内部结构地情况下,对外提供统一访问集合内部数据的方法。迭代器的实现一般包括一个迭代器,用于执行具体的遍历操作,以及一个Collection,用于存储具体的数据。

18.讲一讲责任链模式

①责任链模式用于避免请求发送者与多个请求处理者耦合在一起,让所有请求的处理者持有下一个对象的引用,从而将请求串联成一条链,在有请求发生时,可将请求沿着这条链传递,直到遇到该对象的处理器。
②该模式下用户只需将请求发送到责任链上即可,无需关心请求的处理细节和传递过程,所以责任链模式优雅地将请求的发送和处理进行了解耦。责任链模式常用于Web模式。
③责任链模式包含以下三种角色:Handler接口:规定责任链上要执行的具体方法。AbstractHandler抽象类:持有Handler实例并通过get/set方法将各个具体的业务Handler串联成一个责任链,客户端上的请求在责任链上执行。业务Handler:用户根据具体的业务需求实现的业务逻辑。

19.讲一讲命令模式

①命令模式将请求封装为命令基于事件驱动异步执行,以实现命令的发送者和命令的执行者之间的解耦,提高命令发送执行的效率和灵活度。
②命令模式主要包含以下角色:
抽象命令类:执行命令的接口,定义执行命令的抽象方法。具体命令类:抽象命令类的实现类,持有接收者对象,并在收到命令后调用命令执行者的方法action()实现命令的调用和执行。命令执行者:命令的具体执行者,定义了命令执行的具体方法action()。命令调用者:接收客户端的命令并异步执行。

20.讲一讲备忘录模式

①备忘录模式又叫做快照模式,该模式将当前对象的内部状态保存到备忘录中,以便在需要时能将对象的状态恢复到原先保存的状态。备忘录模式提供了一种保存和恢复状态的机制,常用于快照的记录和状态的存储,在系统发生鼓掌或数据发生不一致时能够方便地将数据恢复到某个历史状态。
②备忘录的核心是设计备忘录类及用于管理备忘录的管理者类,主要角色如下:发起人Originator:记录当前时刻的内部状态,定义创建备忘录和回复备忘录数据的方法。备忘录Memento:负责存储对象的内部状态。状态管理者Storage:对备忘录的历史状态进行存储,定义了保存和获取备忘录状态的功能。注意备忘录只能被保存或恢复,不能进行修改。

21.讲一讲状态模式

①状态模式指给对象定义不同的状态,并为不同的状态定义不同的行为,在对象的状态发生变换时自动切换状态的行为。状态模式是一种对象行为型模式,它将对象的不同行为封装到不同的状态中,遵循单一职责原则。
②具体角色如下:环境: 也叫做上下文,用于维护对象当前的状态,并在对象状态发生变化时触发对象行为的变化。抽象状态:定义接口,用于定义对象中不同状态对应行为。具体状态:抽象状态的实现类

22.讲一讲访问者模式

①访问者模式指将数据结构和数据的操作分离开来,使其在不改变数据结构的前提下动态添加作用于这些元素的操作。访问者模式通过定义不同的访问者实现对数据的不同操作,因此在需要给数据添加新的操作时只需为其定义一个新的访问者即可。
②访问者模式是一种对象行为型模式,主要特点是将数据结构和作用于结构上的操作解耦,使得集合的操作可自由地演化而不影响其数据结构,它适用于数据结构稳定但操作多变的系统中。
③主要角色如下:抽象访问者:定义了一个访问元素的接口,为每类元素都定义了一个访问操作,该操作中的参数类型对应被访问元素的数据类型。具体访问者:抽象访问者的实现类,实现了不同访问者访问元素后具体行为。抽象元素:定义了访问该元素的入口方法,不同访问者类型代表不同访问者。具体元素:实现抽象元素定义的入口方法,根据访问者的不同类型实现不同逻辑业务。

23.讲一讲中介者模式

①中介者模式指对象和对象之间不直接交互,而是通过一个名为中介者的角色来实现,使原有对象之间的关系变得松散,且可以通过定义不同的中介者来改变它们之间的交互。
②主要包含以下角色:抽象中介者:中介者接口,定义了注册同事对象方法和转发同时对象信息的方法。具体中介者:中介者接口的实现类,定义了一个集合保存同事对象,协调各同事角色之间的交互关系。抽象同事类:定义同事的接口类,持有中介者对象,并定义同事对象交互的抽象方法,同时实现同事类的公共方法和功能。具体同事类:抽象同事的实现类,在需要与其他同事对象交互时,通过中介者对象来完成。

24.讲一讲解释器模式

①解释器模式给定一种语言,并定义该语言的语法表示,然后设计一个解释器来解释语言的语法,这种模式常被用于SQL解析、符号处理引擎等。
②解释器模式包含以下主要角色:抽象表达式:定义解释器的接口,约定解释器所包含的操作。终结符表达式:抽象表达式的子类,用来定义语法中和终结符有关的操作,语法中的每一个终结符都应有一个与之对应的终结表达式。非终结符表达式:抽象表达式的子类,用来定义语法中和非终结符有关的操作,语法中的每条规则都有一个非终结符表达式与之对应。环境:定义各个解释器需要的共享数据或公共功能。

JVM

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

java中的编译器和解释器:

Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。

编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。

在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器码然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。

Java源代码->编译器-jvm可执行的Java字节码(即虚拟指令)…–>jvm–jyvm中解释器……>机器可执行的二进制机器码---->程序运行。

采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

为什么要进行年龄分代?

主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法:

  • 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

  • 老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。

新生代又分为Eden和 From Survivor ,To Surivor 三个区。加上老年代就这四个区。数据会首先分配到Eden区当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代【大对象是指需要大量连续内存空间的java对象】)。当Eden没有足够空间的时候就会触发jvm发起一次Minor GC,将Eden区域的数据复制到 To Survivor中,交换To Survivor 和From Survivor的位置,。如果对象经过一次Minor-GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。

Minor GC、Major GC、Full GC区别及触发条件

Minor GC:

  • eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
  • 新创建的对象大小 > Eden所剩空间
  • **补充:**Minor GC时,应用需要挂起,也就是 stop the world:暂停其他用户线程,让垃圾回收线程先工作,垃圾回收工作完成再让用户线程恢复运行,暂停时间比较短,因为回收的大部分是需要回收的对象,复制的对象相对于比较少,最大寿命是15(4bit)分代年龄包含在java的对象头中(mark word)

Major GC:

  • Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。
  • 补充: Major GC时,应用需要挂起,也就是 stop the world:暂停其他用户线程,让垃圾回收线程先工作,垃圾回收工作完成再让用户线程恢复运行,相对于Minor GC暂停时间相对比较长,老年代采用的标记清除算法和标记整理算法,前面也有提到过。

Full GC:

  • Full GC是清理整个堆空间,包括年轻代和老年代

java类加载器有哪些?

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

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

通过继承ClassLoader实现自定义类加载器,重写 findClass 方法 ,自定义类加载的规则。

双亲委派模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g7zKaZmT-1649420221282)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20220221145745160.png)]

当一个类加载器收到加载类的任务时,会先让父类加载器去加载,父类加载器也会优先让本身的父类加载器去加载,依次类推,直至启动类加载器。 类的全路径+类加载器才能确定是否是同一个类

**好处:**这样做的好处就是避免类的重复加载,保护了核心的API库。所有以java.开头的都是禁止加载的

说一下 JVM 运行时数据区 ***

程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。

Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。

Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。

Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。

方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。回收目标主要是常量池的回收和类型的卸载,线程共享

jvm主要参数

-Xmx3550m: 最大堆大小为3550m。

-Xms3550m: 设置初始堆大小为3550m。

-Xmn2g: 设置年轻代大小为2g。

-Xss128k: 每个线程的堆栈大小为128k。

-XX:MaxPermSize:16m 设置持久代大小为16m

-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。

-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。

浅复制和深复制的区别?

Java中的复制分为浅复制和深复制

①浅复制:Java中的浅复制是通过实现Cloneable接口并重写clone方法实现。在浅复制的过程中,对象的基本数据类型的变量值会重新被复制和创建,而引用数据类型仍指向原对象的引用,也就是说浅复制不复制对象的引用数据类型

②深复制:在深复制的过程中,不论是基本数据类型还是引用数据类型,都会被重新复制和创建。简而言之,深复制彻底复制了对象的数据,浅复制的复制不彻底,忽略了引用数据类型

简单描述一下垃圾回收机制 *

在java中,程序员是不需要显示的去释放一个对象的内存,而是由虚拟机自行执行,在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

java中的引用类型有哪些?

  • 强引用(StronglyReference)通过 new 关键出来的对象都是强引用,只要还有引用关系在,无论任何情况都无法被垃圾回收器所回收
  • 软引用(SoftReference) 仅有软引用引用该对象时,在垃圾回收后,内存仍然不足时会再次出发垃圾回收,回收软引用对象 可以配合引用队列来释放软引用自身 ,java中的软引用类【SoftReference】
  • 弱引用(WeakReference) 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身,java中的软引用类 【WeakReference】
  • 虚引用(PhantomReference) 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法 [Cleaner的clean() ] 释放直接内存,java中的虚引用类【PhantomReference】
  • 终结器引用(FinalReference) 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

怎么判断对象是否可以被回收? *

垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死了,需要被回收。
一般有两种方法来判断:

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器+1,引用被释放时计数-1,当计数器为0时就可以被回收,它有一个缺点不能解决循环引用问题;
  • 可达性分析算法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是可以被回收的。java中用的是可达性分析算法

说一下 JVM 有哪些垃圾回收算法?*

  1. 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
  2. 标记复制算法:按照容量划分两个大小相等的内存区域,当其中一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
  3. 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后清除掉端边界以外的内存。
  4. 分代算法:根据对象的存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法

描述一下JVM加载Class文件的原理机制

Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式加载的,除非我们有特殊用法,像反射,就需要显示的加载所需要的类
类装载方式,有两种:

  • 隐式装载:程序在运行过程中当碰到new等方式生产对象时,隐式调用类加载器加载对应的类到jvm中
  • 显示装载:通过class.forname等方法,显示加载需要的类。

说一下类装载的执行过程?*

类装载分为以下5个步骤:

  • 加载:根据查找路径找到相应的class文件然后导入;
  • 验证:检查加载的class文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标识,而直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。

说一下 JVM 调优的工具?

JDK自带了很多监控工具,都位于JDK的bin目录下,其中最常用的是jconsole和jvisualvm这两款视图监控工具

  • jconsole:用于对JVM中的内存,线程和类等进行监控;
  • jvisualvm :JDK自带的全能分析工具,可以分析:内存快照,线程快照,监控内存的变化,gc变化等。

java多线程

说说你对线程优先级理解 *

线程可以划分优先级,优先级高的线程得到的CPU资源比较多,也就是CPU优先执行优先级高的线程对象中的任务。

设置线程优先级有助于帮助线程规划器确定下一次选中哪一个线程优先执行。

java中优先级分为1-10个级别 可以通过setPriority()方法进行设置 ,数字越大代表优先级越高

注意:线程的优先级和代码的先后顺序无关,但是优先级高的不一定先被执行,因为线程是抢占式模型,只是大概率得到cpu执行权的机率比较大。

Java中的锁有什么作用?有哪些分类?

①Java中的锁主要用于保障多并发情况下数据的一致性,线程必须先获取锁才能进行操作,可以保证数据的安全。

②从乐观和悲观的角度可以分为乐观锁和悲观锁。

③从获取资源的公平性可以分为公平锁和非公平锁。

④从是否共享资源的角度可以分为共享锁和排它锁。

⑤从锁的状态角度可分为偏向锁、轻量级锁和重量级锁。同时在JVM中还设计了自旋锁以更快地使用CPU资源。

讲一讲线程中断

①interrupt方法用于向线程发送一个终止信号,会影响该线程内部的中断标识位,这个线程本身不会因为调用了interrupt方法而改变状态,状态的具体变化需要等待接收到中断标识的程序的处理结果判定。

②调用interrupt方法不会中断一个正在运行的线程,只会改变内部的中断标识位的值为true。

③当调用sleep方法使线程处于TIMED-WAITING状态使,调用interrupt方法会抛出InterruptedException,使线程提前结束TIMED-WAITING状态。在抛出该异常前将清除中断标识位,所以在抛出异常后调用isInterrupted方法返回的值是false。

④中断状态是线程固有的一个标识位,可以通过此标识位安全终止线程。比如想终止某个线程时,先调用interrupt方法然后在run方法中根据该线程isInterrupted方法的返回值安全终止线程。

线程安全的集合有哪些?线程不安全的呢?

线程安全的:

  • Vector
  • HashTable
  • Properties
  • JUC下面的并发集合工具类
  • 由 Collections.synchronizedList(list) 转换后的安全集合 里面所有操作元素的方法都由synchronized进行修饰

线性不安全的:

  • ArrayList
  • LinkedList
  • HashMap
  • HashSet
  • TreeMap
  • TreeSet

多线程运行原理

我们都知道 JVM运行时数据区域由: 程序计数器、java虚拟机栈、,本地方法栈,java堆,方法区 所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,java虚拟机就会为其分配一块栈内存,栈内存是线程独有的,他们之间互不干扰。栈先进后出,当这个方法执行完就释放该内存,返回到返回地址继续执行。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 栈帧中由局部变量表,方法执行完后的返回地址,锁记录,操作数栈构成

使用while(true)时如何防止cpu100%空转

  • 使用Thread.sleep(比较小的值) [无需加锁,适用于不同步的场景]
  • 使用yield [ 需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景]
  • 使用wait进行等待 [需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景]

如何合理的终止一个线程

  1. 定义一个Thread monitor ,需要被终止的线程
  2. 在运行的线程中不断判断是否被使用 sInterrupted()设置打断标记,如果是跳出循环,如果是在睡眠中打断使用 interrupt()重新设置打断标记
  3. 外部线程调用 monitor.interrupt() 设置打断标记,告诉他该停止了
  4. 注意:如果线程在sleep 期间被打断,打断标记是不会变的

如何设置一个守护进程 *

  • threadObject.setDemo(true)
  • 不过需要在 start 启动之前调用,否则会出现 IllegalThreadStateException 非法线程状态 异常

什么是临界区?

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

补充:什么是竟态条件?

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

线程的变量安全分析

  • 成员变量:如果没有被多个线程共享则安全,如果被多个线程共享,只有读操作则没有线程安全问题,反之依然
  • 局部变量:线程安全的,如果逃离方法的作用范围需要考虑线程安全问题

线程安全类的方法组合是否线程安全?

是线程非安全的,虽然类本身保证了线程安全,但是不能保证两个方法执行的一个有序性

不可变类有什么特性?

不可变类是线程安全的,因为内部状态不可改变,所以只能读,没有写操作,由此保证线程安全,例如String

什么是java对象头?

在JVM中,对象在内存中的布局分为三块区域:Mark Word、实例数据,对齐填充

实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。

Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qeZ8RlWQ-1649420221283)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20220308184642230.png)]

什么是Monitor?

Monitor是实现synchronized的基础, 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。

  • Monitor中有三个属性:>
    • Owner : 表示当前获得锁的线程
    • WaitSet :被调用wait()方法后需要进行的等待池,需要被notifyAll()或者是notify()唤醒
    • EntryList :和当前的Owner 竞争锁失败的线程会进入到EntryList ,当Owner 线程释放锁后,EntryList 的线程概述竞争锁,非公平竞争

synchronized使用放锁的时机

  • 当前线程的同步方法、代码块执行结束的时候释放
  • 当前线程在同步方法、同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放。
  • 出现未处理的error或者exception导致异常结束的时候释放
  • 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁

如下情况不会释放锁:

  • 程序调用 Thread.sleep() Thread.yield() 这些方法暂停线程的执行,不会释放。
  • 线程执行同步代码块时,其他线程调用 suspend 方法将该线程挂起,该线程不会释放锁 ,所以我们应该避免使用 suspend 来控制线程。

java虚拟机对synchronized如何进行优化?

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

偏向锁: 如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作。但是对于锁竞争比较激烈的场合,偏向锁就失效了,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

**轻量级锁:**倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

**自旋锁:**轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

**锁消除:**消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。比如说在一方法内,对局部变量进行加锁,就会出现锁消除,因为局部变量,只要没有逃离方法的作用范围,则本身就是线程安全的,不需要进行加锁。

unpark和park原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter【0或1】 , _cond 和 _mutex

  • 调用 park 就是判断当前线程是否需要进入阻塞状态
    • 如果_counter=0,进入阻塞状态
    • 如果_counter=1,无需进入阻塞状态
  • 调用 unpark,唤醒由park阻塞的线程
    • 如果这时线程还在阻塞状态【c_counter=0】,就唤醒让他继续向下执行
    • 如果这时线程还在运行【_counter=1】,那么下次他调用 park 时,就无需阻塞,就继续向下执行

Java锁优化的思路和方法

  1. 减少加锁的粒度,只对需要同步的代码块进行加锁
  2. 在两个操作不相关联,无共享数据的时候,可以用多把锁的方式来降低锁的粒度
  3. 锁分离 使用读写锁,区分读、写操作
  4. 锁消除,不要对不可能发生线程安全的代码块进行加锁
  5. 锁粗化 两个同步模块之间如果存在执行时间段相同的代码块,合并两个同步模块,核心思想减少获取锁的次数。

如何定位死锁?

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 进程id 定位死锁

volatile 原理

  • 读屏障会确保指令可见性时,对共享变量的读取,加载的是主存中最新数据
  • 写屏障会确保指令可见性时 ,对共享变量的改动,都同步到主存当中
  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

说说你对CAS的理解

CAS是无锁并发的一个实现基础,是基于乐观锁思想的一个实现,CAS必须借助volatile关键字才能实现比较并交换的目的。
CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下,比如JUC下面的AtomicInteger就是这样实现的。使用Unsafe的unsafe.compareAndSwapInt(对象值,字段偏移量,旧值,新值)可以进行达到比较并且交换的目的。

CAS的缺点:

  • CPU开销过大
  • 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
  • 不能保证代码块的原子性
  • CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized或其他锁。

CAS的优点:

  • 不用进入阻塞状态,减少了线程上下文切换的开销时间,让线程一直在运行状态,无需重新获得锁
  • 注意点CAS线程的核心数不要多于CPU的核心数

扩展:

如果我们需要保护的变量不是整型变量,那就得使用原子引用类:AtomicReference

如何解决ABA问题?

什么是ABA问题?就是一个线程在修改对象引用指向的数据的时候,别的线程无法感知到修改了。

解决方法: 使用 AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,就可以使用AtomicMarkableReference

sleep() wait() join() yield()的区别

1.锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。

2.等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify ()或notifyAll()后等待池的线程才会开始去竞争锁,notify ()是随机从等待池选出一个线程放到锁池而notifyAll()是将等待池的所有线程放到锁池当中。

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

2.sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。

sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interruptexception异常返回,这点和wait是一样的。

3.sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4.sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5.sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信。
6.sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。
7.yield ()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
8.join ()执行后线程进入阻塞状态,例如在线程B中调用线程A的join (),那线程B会进入到阻塞队列,直到线程A结束或中断线程.

对线程安全的理解

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的

堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。

在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。

线程和进程的区别?

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。

Thread和Runable的区别

Thread和Runnable的实质是实现关系,Thread实现了Runnable接口,没有可比性。无论使用Runnable还是Thread,都会new Thread然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现Runnable。

ThreadLocal如何解决内存泄漏?

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PeG0pIpu-1649420221284)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20220221161259359.png)]

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本,threadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)

key使用强引用

当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key使用弱引用

当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set().get(), remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

为什么使用线程池,有什么好处?

1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。

2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。

3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配资源,调优监控。

线程池七大参数:

  • corePoolSize 核心线程数目 (最多保留的线程数,即使执行完任务也会一直存在)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间 - 针对救急线程,救急线程执行完任务后的消亡时间
  • unit 时间单位 - 针对救急线程[临时创建的线程,执行完任务,生存时间一到就结束,等到下次新任务来时再次创建]
  • workQueue 阻塞队列 [需要使用那种数据结构的阻塞队列]
  • threadFactory 线程工厂 (为线程自定义名称)
  • handler 拒绝策略 [当任务队列满时,无法在执行新任务需要执行的策略]

例如:

 ExecutorService threadService = new ThreadPoolExecutor(
                5, // 线程池的核心线程数
                10, // 能容量的最大线程数
                20L, // 空闲线程存活时间
                TimeUnit.SECONDS, // 存活时间单位
                new LinkedBlockingQueue<>(),// 存放提交但未执行任务的队列
                Executors.defaultThreadFactory(), // 创建线程的默认工厂
                new ThreadPoolExecutor.AbortPolicy()); // 拒绝策略

线程的拒绝策略:

CallerRunsPolicy: 该策略不抛出异常,也不会抛弃任务,而是将某些任务回退给调用者处理。

AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执 行流程,影响后续的任务执行。

DiscardPolicy: 该策略默认丢弃无法处理的任务,不抛出异常,如果允许任务丢失,这是一种最好的策略

DiscardOldestPolicy: 丢弃队列最早的未处理任务,把当前任务添加到队列中尝试再提交当前任务。

简述线程池的处理流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DCsRj4uD-1649420221285)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5C%E6%9C%AA%E5%91%BD%E5%90%8D%E6%96%87%E4%BB%B6-1645443143124.jpg)]

线程池中阻塞队列的作用?为什么不是普通队列?

  1. 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了

阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于—直占用cpu资源

  1. 在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

线程池线程复用原理

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

将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。

守护线程的作用是什么?

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。圾回收线程(守护线程)。

注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为它不靠谱;

举例:GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:

(1)来为其它线程提供服务支持的情况;

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

thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个llegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。

在Daemon线程中产生的新线程也是Daemon的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。
Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用ava的线程池。

例如 : Tomcat的Acceptor线程就是守护线程,他是一个接收请求分发请求的一个线程,只有其余的线程还未消亡,他就一直会接收请求,并放到任务队列。

创建线程有哪几种方式?

①. 继承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()方法来获得子线程执行结束后的返回值。

说一下 runnable 和 callable 有什么区别?

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

线程有哪些状态?

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

创建状态:在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。

就绪状态:当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。

阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。

死亡状态:如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪

阻塞又可以分为三种状态:

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

sleep() 和 wait() 有什么区别?

sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了

sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。

notify()和 notifyAll()有什么区别?

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。

优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

线程的 run()和 start()有什么区别?

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。

start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

并发三大特性

**原子性:**一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。

在 Java 中,对基本数据类型的变量读取赋值操作是原子性操作。

如何保证原子性

  • 通过 synchronized 关键字定义同步代码块或者同步方法保障原子性。
  • 通过 Lock 接口保障原子性。
  • 通过 Atomic 类型保障原子性。

可见性: 当一个线程修改了共享变量的值,其他线程能够看到修改的值。

Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。

volatile 变量和普通变量区别

普通变量与 volatile 变量的区别是 volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

如何保证可见性

  • 通过 volatile 关键字标记内存屏障保证可见性。
  • 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性。
  • 通过 Lock 接口保障可见性。
  • 通过 Atomic 类型保障可见性。
  • 通过 final 关键字保障可见性

**有序性:**即程序执行的顺序按照代码的先后顺序执行。

JVM 存在指令重排,所以存在有序性问题。

如何保证有序性

  • 通过 synchronized关键字 定义同步代码块或者同步方法保障可见性。
  • 通过 Lock接口 保障可见性

JUC包下的常用线程池

①. Executors.newFixedThreadPool(int nThreads)

创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。

②. Executors.newCachedThreadPool()

创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。

③. Executors.newSingleThreadExecutor()

这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。

④. Executors.newScheduledThreadPool(int corePoolSize)

创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

JUC下的线程同步辅助工具类

Semaphore:

Semaphore是JDK提供的一个同步工具,它通过维护若干个许可证来控制线程对共享资源的访问。 如果许可证剩余数量大于零时,线程则允许访问该共享资源;如果许可证剩余数量为零时,则拒绝线程访问该共享资源。 Semaphore所维护的许可证数量就是允许访问共享资源的最大线程数量。 所以,线程想要访问共享资源必须从Semaphore中获取到许可证。通常用于资源数量的限制

CountDownLatch :

countDown方法和await方法,CountDownLatch在初始化时,需要指定用给定一个整数作为计数器。当调用countDown方法时,计数器会被减1;当调用await方法时,如果计数器大于0时,线程会被阻塞,一直到计数器被countDown方法减到0时,线程才会继续执行。计数器是无法重置的,当计数器被减到0时,调用await方法都会直接返回。

CyclicBarrier :

循环栅栏 CyclicBarrier CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中 CyclicBarrier 的构造方法第一个参数是目标障碍数,第二个参数为一个屏障点(一个线程),每次执行 CyclicBarrier 计数器(–count),如果count=0,才会执行 屏障点中的代码。

线程池都有哪些状态?

1.RUNNING

  • 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
  • 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0

2.SHUTDOWN

  • 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
  • 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3.STOP

  • 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
  • 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4.TIDYING

  • 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
  • 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5.TERMINATED

  • 状态说明:线程池彻底终止,就变成TERMINATED状态。
  • 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
    线程池各个状态切换框架图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WhJs4H2O-1649420221286)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5CaHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X2pwZy9RQ3U4NDlZVGFJT1B3UFRLSkpYYW9ydzFGSk1OUkthT2libXBwZG04R0lpYjAwUUNneWliclZoRmNVNHR2akFCb1lCVUtpYlZFZlE5VXZBdlQwbGliV0E5TFdnLzY0MA)]

线程池中 submit()和 execute()方法有什么区别?

  • 接收的参数不一样
  • submit有返回值,使用Future来进行结束返回值,而execute没有
  • submit方便Exception处理

在 java 程序中怎么保证多线程的运行安全?

原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
有序性:程序执行的顺序按照代码的先后顺序执行

多线程锁的升级原理是什么?

在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

锁升级的图示过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PUPrUDlo-1649420221286)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5CaHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9RQ3U4NDlZVGFJT1B3UFRLSkpYYW9ydzFGSk1OUkthTzFwYVdpYThyR2RoTkdEeW1KNVdpYk5ZOHBFaWJsNm0yMUZnNUpoNlhZUURIa0hzVHZlUVVmZXVDdy82NDA)]

什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。

怎么防止死锁?

死锁的四个必要条件:

互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源

请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放

不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

ThreadLocal的原理和便用场景

每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值
ThreadLocalMap由一个个Entry对象构成

Entry继承自weakReference<ThreadLocal<?>>,一个Entry由ThreadLocal对象和object构成。

由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中

get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
spring框架在事务开始时会给当前线程绑定一个Jdbc connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离

例如:

    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
   
        for (int i = 0; i < 3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }

    }

    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };



    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i < 5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }

说一下 synchronized 底层实现原理?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

同步代码块是通过 monitorenter monitorexit 指令获取线程的执行权

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法:锁是当前实例对象
  • 静态同步方法:锁是当前类的class对象
  • 同步方法块:锁是括号里面的对象

synchronized 和 volatile 的区别是什么?

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

synchronized 和 Lock 有什么区别?

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁,Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
  • synchronized的只有一个条件变量waitSet ,Lock支持多个条件变量,可以实现精准唤醒某个线程

Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

synchronized 和 ReentrantLock 区别是什么?

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
ReentrantLock可以获取各种锁的信息
ReentrantLock可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。

说一下 atomic 的原理?

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。

Wbe阶段

http请求分为哪几个部分

HTTP请求的组成:状态行、请求头、消息主体三部分组成

HTTP响应的组成:状态行、响应头、响应正文

HTTPS是什么?

HTTPS是以安全为目标的HTTP通道,它在HTTP中加入SSL层以提高数据传输的安全性。HTTP被用于在Web浏览器和网站服务器之间传递信息,但以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此HTTP不适合传输一些敏感信息,比如身份证号码、密码等。为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并对浏览器和服务器之间的通信进行数据加密,以保障数据传输的安全性,其端口一般是443

简述HTTPS的加密流程

①发起请求:客户端在通过TCP和服务器建立连接之后(443端口),发出一个请求证书的消息给服务器,在该请求消息里包含自己可实现的算法列表和其他需要的消息。

②证书返回:服务端在收到消息后回应客户端并返回证书,在证书中包含服务器信息、域名、申请证书的公司、公钥、数据加密算法等。

③证书验证:客户端在收到证书后,判断证书签发机构是否正确,并使用该签发机构的公钥确认签名是否有效,客户端还会确认在证书中列出的域名就是它正在连接的域名。如果客户端确认证书有效,则生成对称密钥,并使用公钥将对称密钥加密。

④密钥交换:客户端将加密后的对称密钥发送给服务器,服务器在接收到对称密钥后使用私钥解密。

⑤数据传输:经过上述步骤,客户端和服务器就完成了密钥对的交换,在之后的数据传输过程中,客户端和服务端就可以基于对称加密(加密和解密使用相同密钥的加密算法)对数据加密后在网络上传输,保证了网络数据传输的安全性

简述HTTP的传输流程

①地址解析:地址解析通过域名系统DNS解析服务器域名从而获得主机的IP地址。例如客户端的浏览器请求http://localhost:8080/index.html,则可分析出:协议名HTTP、主机名localhost、端口8080、对象路径/index.html。

②封装HTTP数据包:解析协议名、主机名、端口、对象路径等并结合本机自己的信息封装成一个HTTP请求数据包。

③封装TCP包:将HTTP请求数据包进一步封装成TCP数据包。

④建立TCP连接:基于TCP的三次握手机制建立TCP连接。

⑤客户端发送请求:在建立连接后,客户端发送一个请求给服务器。

⑥服务器响应:服务器在接收到请求后,结合业务逻辑进行数据处理,然后向客户端返回相应的响应信息。在响应信息中包含状态行、协议版本号、成功或错误的代码、消息体等内容。

⑦服务器关闭TCP连接:服务器在向浏览器发送请求响应数据后关闭TCP连接。但如果浏览器或者服务器在消息头加入了Connection:keep-alive,则TCP连接在请求响应数据后仍然保持连接状态,在下一次请求中浏览器可以继续使用相同的连接发送请求。采用keep-alive不但减少了请求响应的时间,还节约了网络带宽和系统资源。

TCP和UDP的区别

UDP协议:User Datagram Protocol 用户数据报协议,将数据源和目的封装成数据包中,发送数据包到接收端(等于发广播);

  • 一种无需连接的传输层协议(不需要建立连接通道);
  • 不可靠连接协议,只管发送不管对方是否接收;
  • 无连接是不可靠协议,不需要建立连接速度快,所以发送和接收数据的执行效率高;
  • 发送数据大小有限制的不超过64kb;

TCP协议:Transmission Control Protocol传输控制协议,建立连接形成传输数据的通道(等于和陌生人打电话处理事情);

  • 一种面向连接的、可靠的、基于字节流的传输层通讯协议(需要建立连接通道);
  • 可靠连接协议(安全的)
  • 必须建立连接所以执行效率会稍低;
  • 发送数据的大小无限制,越大越慢;
  • 发送数据: 通过通过内的字节输出流OutputStream 写数据到服务器端
  • 建立连接时需三次握手(success),断开连接时需四次挥手;

http三次挥手过程

(1) 序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。

(2) 确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。

(3) 标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:

A)URG:紧急指针(urgent pointer)有效。
(B)ACK:确认序号有效。
(C)PSH:接收方应该尽快将这个报文交给应用层。
(D)RST:重置连接。
(E)SYN:发起一个新连接。
(F)FIN:释放一个连接。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37trq7iG-1649420221287)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5C20201111173843475.png)]

第一次握手

Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

第二次握手

Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

第三次握手

Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了

http四次挥手过程

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7sn6or7H-1649420221287)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5C20201111173906514.png)]

第一次挥手

Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

第二次挥手

Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

第三次挥手

Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

第四次挥手

Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

tcp 为什么要三次握手,两次不行吗?为什么?

为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。

如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。

说一下 tcp 粘包是怎么产生的?

①. 发送方产生粘包

采用TCP协议传输数据的客户端与服务器经常是保持一个长连接的状态(一次连接发一次数据不存在粘包),双方在连接不断开的情况下,可以一直传输数据;但当发送的数据包过于的小时,那么TCP协议默认的会启用Nagle算法,将这些较小的数据包进行合并发送(缓冲区数据发送是一个堆压的过程);这个合并过程就是在发送缓冲区中进行的,也就是说数据发送出来它已经是粘包的状态了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PQLqE5og-1649420221288)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5CaHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9RQ3U4NDlZVGFJTW5PaWFMWkx2Q2F1aWJCNmExTnhJd0xkUUMxZ3FmS0R3dXN2U2xReEVhRG5SU3lTU3ZjSzJWTmxwZEg5TUZmUmFzVGljaGExNEp1dktZUS82NDA)]

②. 接收方产生粘包

接收方采用TCP协议接收数据时的过程是这样的:数据到底接收方,从网络模型的下方传递至传输层,传输层的TCP协议处理是将其放置接收缓冲区,然后由应用层来主动获取(C语言用recv、read等函数);这时会出现一个问题,就是我们在程序中调用的读取数据函数不能及时的把缓冲区中的数据拿出来,而下一个数据又到来并有一部分放入的缓冲区末尾,等我们读取数据时就是一个粘包。(放数据的速度 > 应用层拿数据速度)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sbb1xtCf-1649420221289)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5CaHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9RQ3U4NDlZVGFJTW5PaWFMWkx2Q2F1aWJCNmExTnhJd0xkVjlNaWMxdWFid2NYM0FxWjlUdzlNN2xjYndNUXhGVlVNV21HM1NLTlJpYlZMSFpnY1F2RkRibXcvNjQw)]

说一下 jsp 的 4 种作用域?

JSP中的四种作用域包括page、request、session和application,具体来说:

page代表与一个页面相关的对象和属性。

request代表与Web客户端发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域。

session代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。

application代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。

session 和 cookie 有什么区别?

由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session

1.cookie 是一种发送到客户浏览器的文本串句柄,并保存在客户机硬盘上,可以用来在某个WEB站点会话间持久的保持数据。

2.session其实指的就是访问者从到达某个特定主页到离开为止的那段时间。 Session其实是利用Cookie进行信息处理的,当用户首先进行了请求后,服务端就在用户浏览器上创建了一个Cookie,当这个Session结束时,其实就是意味着这个Cookie就过期了。
注:为这个用户创建的Cookie的名称是aspsessionid。这个Cookie的唯一目的就是为每一个用户提供不同的身份认证。

3.cookie和session的共同之处在于:cookie和session都是用来跟踪浏览器用户身份的会话方式。

4.cookie 和session的区别是:cookie数据保存在客户端,session数据保存在服务器端。

说一下 session 的工作原理?

其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。

如果客户端禁止 cookie 能实现 session 还能用吗?

Cookie与 Session,一般认为是两个独立的东西,Session采用的是在服务器端保持状态的方案,而Cookie采用的是在客户端保持状态的方案。但为什么禁用Cookie就不能得到Session呢?因为Session是用Session ID来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了。

假定用户关闭Cookie的情况下使用Session,其实现途径有以下几种:

手动通过URL传值、隐藏表单传递Session ID。
用文件、数据库等形式保存Session ID,在跨页过程中手动调用

http 响应码 301 和 302 代表的是什么?有什么区别?

答:301,302 都是HTTP状态的编码,都代表着某个URL发生了转移。

区别:

301 redirect: 301 代表永久性转移(Permanently Moved)。
302 redirect: 302 代表暂时性转移(Temporarily Moved )。

forward 和 redirect 的区别?

Forward和Redirect代表了两种请求转发方式:直接转发和间接转发。

直接转发方式(Forward),客户端和浏览器只发出一次请求,Servlet、HTML、JSP或其它信息资源,由第二个信息资源响应该请求,在请求对象request中,保存的对象对于每个信息资源是共享的。

间接转发方式(Redirect)实际是两次HTTP请求,服务器端在响应第一次请求的时候,让浏览器再向另外一个URL发出请求,从而达到转发的目的。

简述 tcp 和 udp的区别?

TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。

TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。

Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。

每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。

TCP对系统资源要求较多,UDP对系统资源要求较少。

OSI 的七层模型都有哪些?

1.应用层:网络服务与最终用户的一个接口。
2.表示层:数据的表示、安全、压缩。
3.会话层:建立、管理、终止会话。
4.传输层:定义传输数据的协议端口号,以及流控和差错校验。
5.网络层:进行逻辑地址寻址,实现不同网络之间的路径选择。
6.数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。
7.物理层:建立、维护、断开物理连接。

get 和 post 请求有哪些区别?*

  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET产生的URL地址可以被加入书签,而POST不可以。
  • GET请求会被浏览器主动缓存,而POST不会,除非手动设置。
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST没有。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  • GET参数通过URL传递,POST放在Request body中。

如何实现跨域?

方式一:图片ping或script标签跨域

图片ping常用于跟踪用户点击页面或动态广告曝光次数。
script标签可以得到从其他来源数据,这也是JSONP依赖的根据。

方式二:JSONP跨域

JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响,而利用

缺点:

  • 只能使用Get请求
  • 不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败
  • JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保

方式三: 全局解决实现WebMvcConfigurer 接口并且实现addCorsMappings()方法

addCorsMappingsimport org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

方式四:直接在Controller接口上加上 @CrossOrigin 注解

如何避免 sql 注入?

  1. PreparedStatement(简单又有效的方法)
  2. 使用正则表达式过滤传入的参数
  3. 字符串过滤
  4. JSP中调用该函数检查是否包函非法字符
  5. JSP页面判断代码

SSM和springBoot

SpringBoot运行项目的几种方式?

  1. 打包用命令或者放到容器中运行

    打成jar包,使用java -jar xxx.jar运行

    打成war包,放到tomcat里面运行

  2. 直接用maven插件运行 maven spring-boot:run

  3. 直接执行main方法运行

SpringBoot配置加载顺序

  1. properties文件
  2. YAML文件
  3. 系统环境变量
  4. 命令行参数

为什么要使用 spring?

1.简介

  • 目的:解决企业应用开发的复杂性
  • 功能:使用基本的JavaBean代替EJB,并提供了更多的企业应用功能
  • 范围:任何Java应用

简单来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。

2.轻量

从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的:典型地,Spring应用中的对象不依赖于Spring的特定类。

3.控制反转

Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。

4.面向切面

Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

5.容器

Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。

6.框架

Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。

所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。

说说你对Aop的理解

系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。

当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。
日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情

说说你对IOC的理解

容器概念、控制反转、依赖注入

**ioc容器:**实际上就是个map (key,value),里面存的是各种对象(在xml里配置的bean节点、@repository,@service、@controller、@component),在项目启动的时候会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放到map里、扫描到上述注解的类还是通过反射创建class对象放到 beanDefinitionMap 里。

这个时候beanDefinitionMap 里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DlI注入(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容器?

1.定义核心注解

2.准备一些基础的核心组件,初始化一个空的工厂,初始化完毕后给你回到接口

3.解析配置文件 扫描注解中指定的扫描路径,获取到class,然后封装为BeanDefinition

4.根据beanDefinition利用反射机制创建这些bean的实例,在创建之前留个接口允许用户自定义返回bean

5.给bean注入属性 注入属性之前后 给用户留回调接口,如果注入其中依赖别的对象,先去创建别的对象,并且做好标记 ,放到工厂中由用户获取。

BeanFactory和ApplicationContext区别

ApplicationContext是BeanFactory的子接口ApplicationContext提供了更完整的功能:

①继承MessageSource,因此支持国际化。
②统一的资源文件访问方式。
③提供在监听器中注册bean的事件。
④同时加载多个配置文件。
⑤载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web层。

  • BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
  • ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了
  • 相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
    ReanEactorv通堂以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用contextLoader。
  • BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用
    但两者之间的区别是: BeanFactory需要手动注册,而ApplicationContext则是自动注册。

spring 有哪些主要模块?

Spring框架至今已集成了20多个模块。这些模块主要被分如下图所示的核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。

Spring Core : 核心类库,提供IOC服务;
Spring AOP : 提供面向切面编程的AOP服务;
Spring Context : 提供框架式的Bean访问方式,以及企业级的功能(JNDI,定时任务等)
Spring MVC : 提供了面向web应用的Model-View-Controller实现;
Spring DAO : 对JDBC抽象化,提供Template模板服务,简化了数据访问的异常处理;
Spring ORM :支持对现有ORM框架的集成和支持;
Spring WEB : 提供了基本的面向Web的综合特征,例如:多文件上传

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8mqKfcAv-1649420221290)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5CaHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9RQ3U4NDlZVGFJTzZqaWI4WFAzcjhoOHNpYjA4MGljaWJIdnV0ZlFKWUNIV0pqQ0VTOWlickZiVzAydXRtQks0bUNsRUNEOWRGV0ppYUtQUDdtUkdqaWJJdVh3YmcvNjQw)]

spring 常用的注入方式有哪些?

Spring通过DI(依赖注入)实现IOC(控制反转),常用的注入方式主要有:

  1. 构造方法注入
  2. setter注入
  3. 基于注解的注入
  4. xml配置文件

spring 中的 bean 是线程安全的吗?

Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究。

@Autowired和@Resource区别

**用途:**做bean的依赖注入时使用

@Resource和@Autowired都可以作为注入属性的修饰,在接口仅有单一实现类时,两个注解的修饰效果相同,可以互相替换,不影响使用。

不同点:

@Autowired 属于Spring的注解   org.springframework.beans.factory.annotation.Autowired

@Resource   不属于Spring的注解,JDK1.6支持的注解    javax.annotation.Resource

@Autowired 默认按类型装配

  • 依赖对象必须存在,如果要允许null值,可以设置它的required属性为false @Autowired (required=false)
  • 当容器中有多个类型一直的bean,可以用@Qualifier注解,指定名称进行注入
  • @Primary是修饰实现类的,告诉spring,如果有多个实现类时,优先注入被@Primary注解修饰的那个

@Resource 默认按名称进行装配,通过name属性进行指定

  • @Resource是Java自己的注解,@Resource有两个属性是比较重要的,分是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。
  • 既指定了@Resource的name属性又指定了type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
  • 只指定@Resource注解的type属性,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常
  • 只是指定了@Resource注解的name,则按name后的名字去bean元素里查找有与之相等的name属性的bean。

spring bean 支持的作用域有哪些?

当通过spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域。Spring支持如下5种作用域:

singleton:单例模式,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例

prototype:原型模式,每次通过容器的getBean方法获取prototype定义的Bean时,都将产生一个新的Bean实例

request:对于每次HTTP请求,使用request定义的Bean都将产生一个新实例,即每次HTTP请求将会产生不同的Bean实例。只有在Web应用中使用Spring时,该作用域才有效

session:对于每次HTTP Session,使用session定义的Bean都将产生一个新实例。同样只有在Web应用中使用Spring时,该作用域才有效

globalsession:每个全局的HTTP Session,使用session定义的Bean都将产生一个新实例。典型情况下,仅在使用portlet context的时候有效。同样只有在Web应用中使用Spring时,该作用域才有效

其中比较常用的是singleton和prototype两种作用域。对于singleton作用域的Bean,每次请求该Bean都将获得相同的实例。容器负责跟踪Bean实例的状态,负责维护Bean实例的生命周期行为;如果一个Bean被设置成prototype作用域,程序每次请求该id的Bean,Spring都会新建一个Bean实例,然后返回给程序。在这种情况下,Spring容器仅仅使用new 关键字创建Bean实例,一旦创建成功,容器不在跟踪实例,也不会维护Bean实例的状态。

如果不指定Bean的作用域,Spring默认使用singleton作用域。Java在创建Java实例时,需要进行内存申请;销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增加。因此,prototype作用域Bean的创建、销毁代价比较大。而singleton作用域的Bean实例一旦创建成功,可以重复使用。因此,除非必要,否则尽量避免将Bean被设置成prototype作用域。

spring 自动装配 bean 有哪些方式?

Spring容器负责创建应用程序中的bean同时通过ID来协调这些对象之间的关系。作为开发人员,我们需要告诉Spring要创建哪些bean并且如何将其装配到一起。

spring中bean装配有两种方式:

隐式的bean发现机制和自动装配
在java代码或者XML中进行显示配置

spring 事务实现方式有哪些?

编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理。

基于 TransactionProxyFactoryBean 的声明式事务管理

基于 @Transactional 的声明式事务管理

基于 Aspectj AOP 配置事务

说一下 spring mvc 运行流程?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6wZy9IpD-1649420221290)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5CaHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9RQ3U4NDlZVGFJTzZqaWI4WFAzcjhoOHNpYjA4MGljaWJIdnVYZjZOODFvYVdSQlRKTjN0WUVIQlBpYjhlSENHTzZNbWx2a25TdWliRW1sUk90ejRJY2ZyeFg5dy82NDA)]

SpringMVC运行流程描述:

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响应用户。

spring mvc 有哪些组件?

Spring MVC的核心组件:

DispatcherServlet:中央控制器,把请求给转发到具体的控制类

Controller:具体处理请求的控制器

HandlerMapping:映射处理器,负责映射中央处理器转发给controller时的映射策略

ModelAndView:服务层返回的数据和视图层的封装类

ViewResolver:视图解析器,解析具体的视图

Interceptors :拦截器,负责拦截我们定义的请求然后做处理工作

@RequestMapping 的作用是什么?

RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

RequestMapping注解有六个属性,下面我们把他分成三类进行说明。

value, method:

value:指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明);
method:指定请求的method类型, GET、POST、PUT、DELETE等;

consumes,produces

consumes:指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;

params,headers

params: 指定request中必须包含某些参数值是,才让该方法处理。
headers:指定request中必须包含某些指定的header值,才能让该方法处理请求。

spring中的Bean是线程安全的么?

Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。

如果Bean是有状态的那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域把"singleton"改为"protopyte’这样每次请求Bean就相当于是new Bean()这样就可以保证线程的安全了。但是这样做的消耗太大,一般不会怎么做,要么就不定义共享数据,如果非要定义,需要加锁。

  • 有状态就是有数据存储功能
  • 无状态就是不会保存数据
  • controller、service和dao层本身并不是线程安全的,只是如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。

Dao会操作数据库Connection,Connection是带有状态的,比如说数据库事务,Spring的事务管理器使用Threadlocal为不同线程维护了一套独立的connection副本,保证线程之间不会互相影响(Spring是如何保证事务获取同一个Connection的)

不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。

spring-bean的生命周期

1.准备一个AnnotatedBeanDefinitionReader并且用于解析扫描到的class,并且注册一些基础的组件

2.将配置类的信息注册到BeanDefinition中去

3.准备一个BeanFactory工厂,注解模式是准备工厂,xml方式是解析配置文件得到BeanDefinition

4.根据扫描配置类中指定的扫描路径,扫描到符合注入条件的class,解析得到BeanDefinition,执行BeanFactoryProcessor的所有回调

5.初始化所有的单例bean 如果你实现了InstantiationAwareBeanPostProcessor接口并且返回对象,接下来的流程不在继续

6.如果没有,确定构造函数,实例化对象,你也可以手动返回,spring有留SmartInstantiationAwareBeanPostProcessor接口

7.实例化完后回调InstantiationAwareBeanPostProcessor的postProcessAfterInstantiation()让你有手动赋值的机会,如果你返回的是false自动注入结束

8.开始进行属性注入,如果在注入的过程中,有依赖其余的bean会先去创建其他的bean

9.赋值完毕后开始回调Aware接口,例如BeanNameAware 属性自动注入完毕后,如果使用了AOP,这里的AOP会创建代理对象

10.初始化完毕后回调BeanPostProcessor的postProcessBeforeInitialization() ()方法你可以检查bean或者是重新返回一个bean

11.调用InitializingBean接口方法给你进行回调

12.如果你有配置init()方法,也会再此时进行回调

13.执行BeanPostProcessor的postProcessAfterInitialization()回调完毕后会添加到单例池

14.所有的单实例bean创建完毕后,如果你实现了SmartInitializingSingleton就会执行所有bean的void afterSingletonsInstantiated();方法,前提是bean是单例的。至此ioc容器的所有组件创建完毕。可以开始进行使用, @Lazy标注的bean需要使用getBean()的时候才会创建。

15.当容器关闭时会调用DisposableBean()中的方法

spring框架中都用到了那些设计模式?

1.简单工厂模式

spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。

2.工厂方法模式

实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getobject()方法,所以返回的不是factory这个bean,而是这个bean.getojbect()方法的返回值。

3.单例模式

spring bean的创建都是默认都是单实例,就是使用的单例模式

4.适配器模式

SpringMVC 中处理器映射器 HandlerMapping 根据配置找到相应的 Handler,返回给前端控制器 DispatcherServlet,前端控制器再传给处理器适配器让它进行处理,处理器适配器会去找到对应的 Handler 去处理,处理后则就会返回一个 ModleAndView 对象。

5.动态代理

在spring的AOP有用到代理模式,jdk动态代理和cglib代理

6.包装模式(装饰模式) *

装饰模式 == 包装模式

**包装模式的概念:**就是指在不修改原类和使用继承的情况下动态的扩展一个对象的功能,通过创建一个包装对象,来包装真实的对象,通过包装对象对原始对象进行功能增强。

包装模式的特点:

  1. 包装对象和被包装对象,具有共同的接口,这样客户端能以和真实对象相同的方式和包装对象进行交互
  2. 包装对象持有真实对象的一个引用
  3. 装饰对象接收客户端的所有请求,在进行额外处理过后,将请求交给真实的对象进行处理
  4. 装饰对象可以在转发请求之前或者之后,对其功能增强,这样就能在不改变原对象的情况下进行功能的增强

以下情况适用包装模式:

  1. 需要扩展一个类的功能,或者添加某个类的附加职责
  2. 需要动态的给一个对象添加功能,然后可以动态的撤销
  3. 不能采用直接继承的情况下,又想对某个类进行功能的增强

实际应用案例:

  1. spring中的BeanWrapper
  2. spring中的BeanHolder
  3. spring中的PropertyValue
  4. jdk源码的IO流

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hfylR1Q9-1649420221290)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20220307003941933.png)]

具体实例:我定义了一个原生的LogInfo对象,我使用装饰模式进行功能的增强,我不仅能调用原生的日志记录,也能在日志记录之前,之后做一些额外的操作。

// Component: 包装模式公共接口
interface Log{
    // 记录日志
    void logInfo();
}

// ConcreteComponent: 被包装的类
class LogInfo implements Log{

    protected String LogMessage;

    public LogInfo(String LogMessage) {
        this.LogMessage = LogMessage;
    }

    @Override
    public void logInfo() {
        System.out.println("日志信息 :"+LogMessage);
    }
}

// Decorator: 抽象包装类,持有被包装类的引用
 class LogDecorator implements Log {

    protected  Log log;
    protected  String LogMessage;
    public   LogDecorator(Log log){
        this.log = log;
    }

    @Override
    public void logInfo() {
        // 将请求交给被包装的类进行处理
        if (log!=null){
            log.logInfo();
        }
    }
}

// ConcreteDecorator: 具体装饰类[对其功能增强]
class LogInfoAfter extends LogDecorator{

    protected String extraMessage;
    public LogInfoAfter(Log log,String extraMessage) {
        super(log);
        this.extraMessage = extraMessage;
    }

    @Override
    public void logInfo() {
        super.logInfo();
        System.out.println("日志信息 : "+extraMessage);
    }
}
// ConcreteDecorator: 具体装饰类[对其功能增强]
class LogInfoBefore extends LogDecorator{

    protected String extraMessage;
    public LogInfoBefore(Log log,String extraMessage) {
        super(log);
        this.extraMessage = extraMessage;

    }

    @Override
    public void logInfo() {
        System.out.println("日志信息 : "+extraMessage);
        super.logInfo();
    }
}

// 客户端调用
public class DecoratorTest {
    public static void main(String[] args) {
        // 具体的日志信息
        LogInfo Log = new LogInfo("admin用户登录");
        // 被包装的抽象召唤师 将具体要执行的逻辑交给被包装的对象
        LogDecorator logDecorator = new LogDecorator(Log);
        // 在执行具体逻辑之前进行增强
        LogInfoBefore logInfoBefore = new LogInfoBefore(logDecorator,"记录日志之前的操作");
        // 在执行具体逻辑之前进行增强
        LogInfoAfter logInfoAfter = new LogInfoAfter(logDecorator,"记录日志之后的操作");
        // 在基本功能之前做操作的增强实现类
        logInfoBefore.logInfo();
        System.out.println("---------------------");
        // 在基本功能之后做操作的增强实现类
        logInfoAfter.logInfo();

        System.out.println("---------------------");
        // 原始的基本功能
        logDecorator.logInfo();

    }
}

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

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

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

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

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

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

spring事务隔离级别就是数据库的隔离级,外加一个默认级别

  • isolation_default:使用数据库默认的事务隔离级别,spring的默认事务隔离级别
  • Read Uncommitted(读未提交):一个事务可以读取另一个未提交事务的数据。
  • Read Committed(读已提交):一个事务要等另一个事务提交后才能读取数据。
  • Repeatable Read(可重复读):在开始读取数据(事务开启)时,不再允许修改操作。
  • Serializable(序列化读):Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。

数据库的配置隔离级别是read Uncommitted,spring配置的隔离级别是Repeatable Read,请问这时隔离级别是以哪一个为准?
以spring配置的为准,如果spring设置的隔离级别数据库不支持,效果取决于数据库。

mysql默认的事务处理级别是’REPEATABLE-READ’,也就是可重复读

oracle 默认系统事务隔离级别是READ COMMITTED,也就是读已提交

spring事务的传播机制

多个事务方法相互调用时,事务如何在这些方法间传播

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

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。TransactionDefinition接口定义了如下几个表示传播行为的常量:

  • REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

  • NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

  • MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

  • NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

  • SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

  • REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。

  • NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED。

这里需要指出的是,以 NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。另外,外部事务的回滚也会导致嵌套子事务的回滚。

// 在service上开启事务注解
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.NESTED)
// 主启动类上开启注解
@EnableTransactionManagement 

spring事务失效

1.使用this调用方法,没有用到代理对象,会导致事务并不会被管理到

2.方法不是public修饰的 @Transactional只是适用于public修饰符修饰的,如果非要使用,需要使用Aspectj进行代理

3.数据库本身不支持事务

4.出现异常被 try catch 捕捉到,并没有抛出去

spring bean自动装配方式

依赖注入:

  1. @Autowired 标注在构造方法上 使用构造注入
  2. @Autowired 标注在setter方法上 使用set方法进行注入
  3. @Autowired 标注在属性上 使用字段的方式自动注入
  4. @Resource 可以放在属性和方法上

使用@Service @Controller @RestController @Component @Repository 标注在类上

spring mvc 和 struts 的区别是什么?

拦截机制的不同

Struts2是类级别的拦截,每次请求就会创建一个Action,和Spring整合时Struts2的ActionBean注入作用域是原型模式prototype,然后通过setter,getter吧request数据注入到属性。Struts2中,一个Action对应一个request,response上下文,在接收参数时,可以通过属性接收,这说明属性参数是让多个方法共享的。Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了,只能设计为多例。

SpringMVC是方法级别的拦截,一个方法对应一个Request上下文,所以方法直接基本上是独立的,独享request,response数据。而每个方法同时又何一个url对应,参数的传递是直接注入到方法中的,是方法所独有的。处理结果通过ModeMap返回给框架。在Spring整合时,SpringMVC的Controller Bean默认单例模式Singleton,所以默认对所有的请求,只会创建一个Controller,又因为没有共享的属性,所以是线程安全的,如果要改变默认的作用域,需要添加@Scope注解修改。

Struts2有自己的拦截Interceptor机制,SpringMVC这是用的是独立的Aop方式,这样导致Struts2的配置文件量还是比SpringMVC大。

底层框架的不同

Struts2采用Filter(StrutsPrepareAndExecuteFilter)实现,SpringMVC(DispatcherServlet)则采用Servlet实现。Filter在容器启动之后即初始化;服务停止以后坠毁,晚于Servlet。Servlet在是在调用时初始化,先于Filter调用,服务停止后销毁。

性能方面

Struts2是类级别的拦截,每次请求对应实例一个新的Action,需要加载所有的属性值注入,SpringMVC实现了零配置,由于SpringMVC基于方法的拦截,有加载一次单例模式bean注入。所以,SpringMVC开发效率和性能高于Struts2。

spring boot ,springMVC,Spring有什么区别

spring是一个lOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题、更方便将不同类不同方法中的共同处理抽取成切面、自动注入给方法执行比如日志、异常等

springmvc是spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接收请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端

springboot是spring提供的一个快速开发工具包,让程序员能更方便、更快速的开发spring+springmvc应用,简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制) 、redis、mongodb、es,可以开箱即用,自动注入,减少配置。

springBoot和SpringCloud的区别?

springBoot和springCloud:

  1. spring boot使用了默认大于配置的理念,集成了快速开发的spring多个插件,同时自动过滤不需要配置的多余的插件,简化了项目的开发配置流程,一定程度上取消xml配置,是一套快速配置开发的脚手架,能快速开发单个微服务;

  2. spring cloud大部分的功能插件都是基于springBoot去实现的,springCloud关注于全局的微服务整合和管理,将多个springBoot单体微服务进行整合以及管理; springCloud依赖于springBoot开发,而springBoot可以独立开发;

springmvc常用注解

@ControllerAdvice 加载异常类上,做异常统一处理
@ExceptionHandler 标注在异常方法上,需要监控什么类型的异常
@Controller 控制器注解,带有@Controller注解的控制器负责处理由DispatcherServlet 分发的请求
@RestController 控制器注解,用于返回json数据,等同于@ResponseBody 和@Controller
@ResponseBody 可以标注在控制层中的方法上,返回json数据
@RequestBody 标注在请求参数位置,接收json类型的数据,转换为对象,注意必须的是post请求
@RequestParam 从请求里面获取参数
@RequestMapping 用于处理请求地址映射,可用于类或方法上。用于类上,表示该类中的所有响应请求的方法都是以该路径作为父路径
@PathVariable 用来获得请求的URL中的动态参数,从路径中获取变量。

springMVC主要组件

Handler:也就是处理器。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。在Controller层中@RequestMapping标注的所有方法都可以看成是一个Handler,只要可以实际处理请求就可以是Handler

  1. HandlerMapping
    initHandlerMappings(context),处理器映射器,根据用户请求的资源uri来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事。里面存放了url和controller的对应关系信息
  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. MultipartResolver
    initMultipartResolver(context),用于处理上传请求。处理方法是将普通的request包装成
    MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。
  8. FlashMapManager
    initFlashMapManager(context),用来管理FlashMap的,FlashMap主要用在redirect中传递参数。

springBoot自动配置原理

  1. @SpringBootApplication 是由 @EnableAutoConfiguration @SpringBootConfiguration @ComponentScan组成的
  2. 最核心的就是@AutoConfigurationPackage 里面导入了一个 AutoConfigurationPackages.Registrar.class 组件
  3. AutoConfigurationPackages.Registrar.class 实现了ImportBeanDefinitionRegistrar 会在registerBeanDefinitions()中进行自动注入
  4. 会使用spi机制扫描 MET-INFO/factories下面的配置信息,里面配置的全是springBoot给我们写好的配置类的全路径名称,先进行全部扫描
  5. 根据我们配置的排除信息和根据一些条件过滤注解,然后把我们需要的组件给我们注册到容器中。

如何理解springBoot的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。

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

节省了下载安装tomcat,应用也不需要再打war包,然后放到webapp目录下再运行只需要一个安装了Java的虚拟机,就可以直接在上面部署应用程序了
springboot已经内置了tomcat.jar,运行main方法时会去启动tomcat,并利用tomcat的spi机制加载springmvc。

减少了繁琐的配置文件,如果需要对嵌入式服务器进行配置,springBoot也会留有切入点给我们进行手动配置。比如说实现TomcatConnectorCustomizer接口,然后设置配置,让后将该bean加入到容器中,Tomcat在启动的时候,配置参数时,就会去容器中自动注入。

Mybatis-Plus条件构造器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BYdubMI7-1649420221290)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20220305222809019.png)]

Wrapper : 条件构造父类。
AbstractWrapper :QueryWrapper(LambdaQueryWrapper)和UpdateWrapper(LambdaUpdateWrapper) 的父类用于生成 sql 的 where 条件, entity 属性也用于生成 sql 的 where 条件。
AbstractLambdaWrapper :Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。
LambdaQueryWrapper :Lambda形式的查询Wrapper。
LambdaUpdateWrapper :Lambda 形式的更新Wrapper。
QueryWrapper :继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件及 LambdaQueryWrapper, 可以通过 new QueryWrapper().lambda() 方法获取。
UpdateWrapper : 继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件及 LambdaUpdateWrapper, 可以通过 new UpdateWrapper().lambda() 方法获取。

mybatis的优缺点

优点:

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

  2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;

  3. 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。

  4. 能够与Spring很好的集成;

  5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点:

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

mybatis执行流程

  1. 读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息;
  2. 加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表;
  3. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory;
  4. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法;
  5. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护;
  6. MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息;
  7. 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程;
  8. 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程;

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使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。

MyBatis的缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还
是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。但是Mybatis-plus 弥补了这个缺点。

mybatis ${ }和#{ }区别

#是预编译处理、是占位符,${}是字符串替换、是拼接符。

Mybatis在处理#{}时,会将sq/中的#替换为?号,调用PreparedStatement来赋值;Mybatis在处理 时 , 就 是 把 时,就是把 替换成变量的值,调用Statement来赋值;

#的变量替换是在DBMS中、变量替换后,#脉对应的变量自动加上单引号 的 变 量 替 换 是 在 D B M S 外 、 变 量 替 换 后 , 的变量替换是在DBMS外、变量替换后, DBMS对应的变量不会加上单引号使用#{}可以有效的防止SQL注入,提高系统安全性。

Mybatis 动态sql有什么用?有哪些动态sql?

  • Mybatis 动态sql可以在Xml映射文件内,以标签的形式编写动态 sql;

  • 执行原理是根据表达式的值完成逻辑判断并动态拼接 sql 的功能;

Mybatis提供了 9 种动态 sql 标签:

  • trim:可实现where/set标签的功能;

    • prefix:表示在trim标签包裹的SQL前添加指定内容
    • suffix:表示在trim标签包裹的SQL末尾添加指定内容
    • prefixOverrides:表示去掉(覆盖)trim标签包裹的SQL指定首部内容,去掉多个内容写法为and |or(中间空格不能省略)(一般用于if判断时去掉多余的AND |OR)
    • suffixOverrides:表示去掉(覆盖)trim标签包裹的SQL指定尾部内容(一般用于update语句if判断时去掉多余的逗号)
  • where:类似于where,只有一个以上的if条件满足的情况下才去插入WHERE关键字;

  • set:用于解决动态更新语句存在的符号问题;

  • foreach:对集合进行遍历;

  • if:当参数满足条件才会执行某个条件;

  • choose:按顺序判断其内部when标签中的test条件是否成立,如果有一个成立,则choose结束;

  • when:条件用于判断是否成立;

  • otherwise:如果所有的when条件都不满足时,则执行otherwise中的SQL;

  • bind:可以从OGNL(对象图导航语言)表达式中创建一个变量并将其绑定到上下文

mybatis插件运行原理

答: Mybatis 只支持针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis 使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的 invoke()方法,拦截那些你指定需要拦截的方法。


/**
 * type:需要拦截的对象
 * method:需要拦截那个方法
 * args:该方法需要的参数
 */
@Intercepts({
@Signature(type = StatementHandler.class,method = "parameterize",args = java.sql.Statement.class)
})
public class MyInterceptor implements Interceptor {
    /**
     *1.插件原理:
     * mybatis四大核心对象:Executor,ParameterHandler,ResultHandler,StatementHandler
     *
     * 1.每个对象不是直接创建出来的,而是会经过
     * statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);的封装
     *
     * 2.获取到所有的Interceptor(拦截器),插件需要实现的接口
     * 调用  target = interceptor.plugin(target);方法返回target对象
     *
     * 3.插件机制,我们可以使用插件为目标对象创建一个代理对象,类似于AOP切面编程
     * 我们可以为四大对象创建出代理对象,代理对象可以拦截到四大对象的每一个执行
     *    // 获取到所有的拦截器
     *      public Object pluginAll(Object target) {
     *     for (Interceptor interceptor : interceptors) {
     *       target = interceptor.plugin(target);
     *     }
     *     return target;
     *   }
     */
    /**
     * 拦截目标对象的执行
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {


        System.out.println("(被拦截的方法)MyInterceptor--->"+invocation.getMethod());

        // 获取到目标对象
        Object target = invocation.getTarget();
        // 获取到target的元数据
        MetaObject metaObject = SystemMetaObject.forObject(target);
        // 获取到target的某个元数据
        metaObject.getValue("parameterHandler.parameterObject");
        // 设置target的某个元数据 设置参数,原本查询的是1号订单现在改变查询数据为4号订单
        metaObject.setValue("parameterHandler.parameterObject",4);

        // 执行目标方法
        Object proceed = invocation.proceed();
        // 执行后的返回值
        return proceed;
    }

    /**
     *  包装目标对象,为目标对象创建一个代理对象
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("MyInterceptor(将要被包装的对象)--->"+target);
        // 我们可以借助Plugin的warp方法来使当前的Interceptor包装我们的目标对象
        Object wrap = Plugin.wrap(target, this);
        return wrap;
    }


    /**
     *  将插件注册时的property属性设置进来
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

        System.out.println(" 插件配置的信息 "+properties);
    }
}

在xml中进行配置

    <!--配置分页插件-->
    <plugins>
      <plugin 
        <plugin interceptor="com.mybatis.interceptor.MyInterceptor">
            <property name="id" value="1001"/>
            <property name="name" value="admin"/>
        </plugin>
    </plugins>

springCloud有哪些技术? *

  1. Eureak 注册中心
  2. Consul 服务发现
  3. Ribbon 负载均衡
  4. OpenFeign 远程调用
  5. Hytrix 服务降级与熔断
  6. GateWay 路由配置
  7. Config 分布式配置中心
  8. Bus 消息总线
  9. .Sleuth链路跟踪
  10. Nacos 注册中心,配置中心
  11. Sentinel 服务限流
  12. seata 分布式事务

springCloud分布式事务流程 *

基于Redis做分布式锁:

  SETNX key value
  含义(setnx = SET if Not eXists):
      将 key 的值设为 value ,当且仅当 key 不存在。
	 若给定的 key 已经存在,则 SETNX 不做任何动作。
	 SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
       
       返回值:
         设置成功,返回 1 。
		设置失败,返回 0 。

为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间

seata分布式事务处理过程的一ID+三组件模型:

  • Transaction ID XID 全局唯一的事务ID
  • 三组件概念
    • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
    • TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
    • RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

处理过程:

  • TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
  • XID在微服务调用链路的上下文中传播;
  • RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
  • TM向TC发起针对XID的全局提交或回滚决议;
  • TC调度XID下管辖的全部分支事务完成提交或回滚请求。

十杯水其中一杯有毒如何快速找出?*

**解题思路:**将这十杯水分别从1开始进行编号,将最后一杯水的序号转化为对应的二进制,有多少位,就需要多少只老鼠,注意:并不是一只老鼠只能喝一杯水,而是一只老鼠可以喝多杯水,但是只喝一点。
主要考察二进制的一个特点,以及二进制数的一个表示范围

第一只老鼠喝个位数为1的水
第二只老鼠喝十位数为1的水
第三只老鼠喝百位数为1的水
第四只老鼠喝千位数为1的水

第一杯: 0001
第二杯: 0010
第三杯: 0011
第四杯: 0100
第五杯: 0101
第六杯: 0110
第七杯: 0111
第八杯: 1000
第九杯: 1001
第十杯: 1010

  • 如果第一只老鼠死了,其余的老鼠活着,那就是第一瓶有毒,因为老鼠喝的是个位数为1的水杯,所以推算出二进制为:0001 转化为对应的十进制就是:1 所以是第一杯水

  • 如果第二只老鼠死了,其余的老鼠活着,那就是第二瓶有毒,因为老鼠喝的是十位数为1的水杯,所以推算出二进制为:0010 转化为对应的十进制就是:2 所以是第二杯水

  • 接下来依次类推

springBoot定时任务 *

  • 在配置类上标注@EnableScheduling注解

  • 自定义一个定时任务类,并且加入组件中

  • 在需要执行的任务方法上加上@Scheduled,并且指定好cron表达式

  • 多线程执行的方式

    • 写一个配置类,在配置类上加上@EnableAsync注解,在该类中向容器添加一个线程池
    • 在需要执行的任务或者方法上加上@Asnyc即可

    微服务调用流程:

    spring cloud在调用接口的过程中,大体经过了5个步骤,其中经过了多个组件。
    步骤1、接口化请求调用
    步骤2、Feign
    步骤3、Hystrix
    步骤4、Ribbon
    步骤5、HttpClient

    具体流程如下

    1、当加了@FeignClient注解的接口被调用时,在框架内部会把请求转换为Feign的请求实例feign.Request,交给Feign框架管理。

    2、根据调用接口的注解@FeignClient后面的注册名称,找到注册中心的功能模块,然后根据调用的接口的方法声明去该功能模块寻找相应的方法体,此时并未执行。

    3、在寻找方法体的过程中,如果上述功能模块服务突然挂掉了,此时就会使用Hystrix组件,请求被Hystrix代理拦截,判断请求是否需要进行熔断,若需要进行熔断,则熔断后并返回异常;否则则传递给Ribbon组件。

    4、Ribbon组件的主要作用是来根据请求的特点,对请求进行负载均衡处理(分给多个服务器处理,防止一台服务器被挤爆)。

    5、最后通过HttpClient,根据Ribbon传过来的请求(此时该请求已经指定了服务地址),开始真正执行请求;

MYSQL

char和varchar区别?*

char是一种固定长度的类型,无论储存的数据有多少都会固定长度,如果插入的长度小于定义长度,则可以用空格进行填充。而varchar是一种可变长度的类型,当插入的长度小于定义长度时,插入多长就存多长。

  • 最大长度:char最大长度是255字符,varchar最大长度是65535个字节;
  • 定长:char是定长的,不足的部分用隐藏空格填充,varchar是不定长的;
  • 空间使用:char会浪费空间,varchar会更加节省空间;
  • 查找效率:char查找效率会很高,varchar查找效率会更低;
  • 尾部空格:char插入时可省略,vaechar插入时不会省略,查找时省略;

如何新增一个字段?

ALTER TABLE table_name ADD COLUMN column_name VARCHAR(100) DEFAULT NULL COMMENT '新加字段' AFTER old_column;

主键递增问题

  • 一张表里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 Mysql 重启,再 insert 一条记录,这条记录的ID是18还是15?
  • 如果表的类型是MyISAM,那么是18;因为MyISAM表会把自增主键的最大ID记录到数据文件里,重启MySQL自增主键的最大ID也不会丢失;
  • 如果表的类型是InnoDB,那么是15;因为InnoDB表只是把最大ID记录到内存中,所以重启数据库或者对表进行optimize(优化)操作,都会导致最大的ID丢失;

UNION和UNION ALL的区别

合并两个结果集的条件:合并的两个结果集,列数必须相同,列类型、列名可以不同

  • UNION: 合并t1 和t2两张表的结果。纵向合并,去除重复的记录
  • UNION ALL: 合并t1 和t2两张表的结果,合并结果集,不去除重复记录

MySQL一张表最多能存多少数据?

  • MySQL本身并没有对单表最大记录数进行限制,这个数值取决于你的操作系统对单个文件的限制本身。业界流传是500万行。超过500万行就要考虑分表分库了。阿里规范中提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表;

SQL锁的优化策略

  • 读写分离;
  • 分段加锁;
  • 减少锁持有的时间;
  • 多个线程尽量以相同的顺序去获取资源不能将锁的粒度过于细化,不然可能会出现线程的加锁和释放次数过多,反而效率不如一次加一把大锁;

MySQL根据生日计算年龄

SELECT TIMESTAMPDIFF(YEAR, birthday, CURDATE())

只需把第二个参数“birthday”更换成你要计算的生日日期

MYSQL如何实现去重 *

1.使用group by

2.distinct 加载需要去重的字段上

mysql 的内连接、左连接、右连接有什么区别?

  • Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
  • left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
  • right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。

说一下数据库的三大范式

  • 第一范式条件:必须不包含重复组的关系,即每一列都是不可拆分的原子项。。
  • 第二范式条件:关系模式必须满足第一范式,并且所有非主属性都完全依赖于主键。注意,符合第二范式的关系模型可能还存在数据冗余、更新异常等问题。
  • **第三范式的条件:**关系模型满足第二范式,所有非主属性对任何候选关键字都不存在传递依赖。即每个属性都跟主键有直接关系而不是间接关系

非聚簇索引一定会回表查询吗?

不一定,如果查询语句的字段全部命中了索引,那么就不必再进行回表查询(覆盖索引就是这么回事)。

索引基本原理

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

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

官方介绍索引是帮助MySQL高效获取数据的数据结构。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度。
一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往是存储在磁盘上的文件中的(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。

我们通常所说的索引,包括聚集索引、覆盖索引、组合索引、前缀索引、唯一索引等,没有特别说明,默认都是使用B+树结构组织(多路搜索树,并不一定是二叉的)的索引。

索引的优势和劣势

优势:

  • 可以提高数据检索的效率,降低数据库的IO成本,类似于书的目录。
  • 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。
    • 被索引的列会自动进行排序,包括【单列索引】和【组合索引】,只是组合索引的排序要复杂一些。
    • 如果按照索引列的顺序进行排序,对应order by语句来说,效率就会提高很多。

劣势:

  • 索引会占据磁盘空间
  • 索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件。

MySQL聚簇索引和非聚簇索引

聚簇索引和非聚簇索引默认都是B+树的数据结构

聚簇索引: 将数据存储与索引放到了一块、并且是按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的。也称聚簇索引。聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。

Innodb引擎的聚簇索引实际上存放了B+树索引和数据行。所以由于无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 在Innodb引擎下,有主键时,根据主键创建聚簇索引;没有主键时,会用一个唯一且不为空的索引列做为主键,成为此表的聚簇索引,如果以上两个都不满足,那Innodb自己创建一个虚拟的聚集索引。

非聚簇索引: 也称非聚簇索引(有时也叫二级索引)。除了聚簇索引以外的索引,统称为非聚簇索引。

在聚簇索引之上创建的索引称之为辅助索引,**辅助索引访问数据总是需要二次查找。**辅助索引叶子节点存储的不再是行的物理位置,而是主键值。通过辅助索引首先找到的是主键值,再通过主键值找到数据行的数据页,再通过数据页中的Page Directory找到数据行。

Innodb辅助索引的叶子节点并不包含行记录的全部数据,叶子节点除了包含键值外,还包含了相应行数据的聚簇索引键。辅助索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个辅助索引。

聚簇索引的优势:

  • 优势:

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

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

聚簇索引和非聚簇索引的区别:

  • 聚簇索引一个表只能有一个,非聚簇索引一个表可以有多个。
  • 聚簇索引的叶子节点存放的是主键值和数据行,一般为表的主键(主索引),支持覆盖索引。
  • 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针,因此在使用辅助索引进行查找时,需要先查找到主键值,然后在通过主索引进行查找。

如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MylSAM占优势些,因为索引所占空间小,
这些操作是需要在内存中完成的。

MYSQL索引的数据结构各自优势

索引的数据0结构和具体存储引擎的实现有关,在MySOL中使用较多的索引有Hash索引,B+树索引等,InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择邮BTee索引。

B+树:
B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互链接。在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库、文件系统等场景。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzXwoqB6-1649420221291)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20220222170302468.png)]

哈希索引:
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。

如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;前提是键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;

哈希索引也没办法利用索引完成排序,以及like 'xxx%'这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);
哈希索引也不支持多列联合索引的最左匹配规则;

B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在哈希碰撞问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CKFy2Rle-1649420221292)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20220222170216611.png)]

场景区分

  • 大多数场景下,都会有组合查询,范围查询、排序、分组、模糊查询等查询特征,Hash 索引无法满足要求,建议数据库使用B+树索引。
  • 在离散型高,数据基数大,且等值查询时候,Hash索引有优势。

索引设计原则

查询更快、占用空间更小

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

mysql索引失效场景

  1. like查询 %写在前面 例如: select * from table where name like ‘%张’;
  2. 在索引列上进行计算或者执行函数
  3. 当全表扫描速度大于使用索引的情况下
  4. 使用组合索引的时候,没有遵循最左匹配原则
  5. 索引列的数据发生强制类型转换
  6. 在使用查询的where条件后面使用范围查询,如果是等值查询则不会失效
  7. or语句前后没有同时使用索引。当or左右查询字段只有一个是索引,该[索引失效,只有当or左右查询字段均为索引时,才会生效
  8. 相join的两个表的字符编码不同,不能命中索引,会导致笛卡尔积的循环计算

MySQL锁的分类

基于锁的属性分类: 共享锁、排他锁。
基于锁的粒度分类: 行级锁(INNODB)、表级锁(INNODB、MYISAM)、页级锁(BDB引擎)、记录锁、间隙锁、临键锁。
基于锁的状态分类: 意向共享锁、意向排它锁。

基于锁的属性分类:

  • 共享锁(Share Lock)
    共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。读读共享
  • 排他锁(exclusive Lock)
    排他锁又称写锁,简称x锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。只有涉及到写的操作就是排他锁, 如: 读写互斥,写写互斥

基于锁的粒度分类:

  • **表锁:**是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问;

  • 特点:粒度大,加锁简单,容易冲突; 不会出现死锁,发生锁冲突几率高,并发低。

    • # 加表锁
      lock table test read;
      # 释放表锁
      

    unlock tables;

    
    
    
    
  • **行锁:**是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问

SELECT UserID,Password,Age FROM AccountsDB.Accounts_InFo WHERE Accounts = ‘hello2’ FOR UPDATE;




- 特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高; 会出现死锁,发生锁冲突几率低,并发高。

- **记录锁:**记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。精准条件命中,并且命中的条件字段是唯一索引 加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。

-  特点:锁住索引记录,而不是真正的数据记录,锁是非主键索引,会在索引记录上加锁后,在去主键索引上加锁,   表上没有索引,会在隐藏的主键索引上加锁,    如果要锁的列没有索引,进行全表记录加锁

- ```sql
  SELECT * FROM table WHERE id = 1 FOR UPDATE;
  ```

- **间隙锁:** 属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成个区间,<font color='red'>遵循左开右闭原则</font>。
范围查询并且查询未命中记录,查询条件必须命中索引、间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。
触发条件:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生如下图的问题,在同一个事务里,A事务的两次查询出的结果会不一样。
比如表里面的数据ID为1,4,5 ,7,10 ,那么会形成以下几个间隙区间,-n-1区间,1-4区间,7-10区间,10-n区间( -n代表负无穷大,n代表正无穷大

-  ```sql
  SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
  # 上面语句就会阻止其他事物插入一个c1列的值在10-20之间,无论之前有没有值存在,因为这之间的所有间隙都已经被锁
  ```

- **临建锁:** 也属于行锁的一种,并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住
触发条件:范围查询并命中,查询命中了索引。
结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。



如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁。

- 意向共享锁
  当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。
- 意向排他锁
  当一个事务试图对整个表进行加排它锁之前,首先需要获得这个表的意向排它锁。

- **全局锁:** 全局锁就对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的MDL的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

- 加锁:flush tables with read lock;
- 解锁:unlock tables;

```sql
# 检测死锁
show variables like 'innodb_deadlock_detect';

mysql执行计划怎么查看

执行计划就是sql的执行查询的顺序,以及如何使用索引查询,返回的结果集的行数,在执行SQL前加上explain关键字进行查看

  • id : 是一个有顺序的编号,是查询的顺序号,有几个select就显示几行。id的顺序是按select 出现的顺序增长的。id列的值越大执行优先级越高越先执行,id列的值相同则从上往下执行,id列的值为NULL最后执行。

  • **selectType:**表示查询中每个select子句的类型

    • SIMPLE:表示此查询不包含UNION查询或子查询
    • PRIMARY:表示此查询是最外层的查询(包含子查询)
    • SUBQUERY:子查询中的第一个SELECT
    • UNION:表示此查询是UNION的第二或随后的查询
    • DEPENDENT UNION: UNION中的第二个或后面的查询语句,取决于外面的查询
    • UNION RESULT,UNION的结果
    • DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外面的查询.即子查询依赖于外层查询的结果
    • DERIVED: 衍生,表示导出表的SELECT (FROM子句的子查询)
  • table: 显示这一行的数据是关于哪张表的

  • type: 访问类型排列,显示查询使用了何种类型

    • type:显示的是访问类型,是较为重要的一个指标,结果值从最好到最坏依次是:system>const>eq_ref>ref>fultext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>ALL
    • 挑重要的来说:system>const>eq_ref>ref>range>index>ALL,一般来说,得保证查询至少达到range级别,最好能达到ref。
      • system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现,这个也可以忽略不计
      • const:表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量
      • eq_ref:唯一性索引,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描
      • ref:非唯一索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体。
      • range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引一般就是在你的where语句中出现了 between、<、>、in等的查询这种范围扫描索引扫描比全表扫描要好,因为他只需要开始索引的某一点,而结束于另一点,不用扫描全部索引
      • index:Full Index Scan,index与ALL区别为index类型只遍历索引树。这通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然all和index都是读全表,但index是从索引中读取的,而all是从硬盘数据库文件中读的
      • all:FullTable Scan,将遍历全表以找到匹配的行(全表扫描)
  • possible_keys: (可能用到的索引)

    • 显示可能应用在这张表中的索引,一个或多个
    • 若查询涉及的字段上存在索引,则该索引将被列出,但不一定被查询实际使用
  • key: 实际所用到的索引

    • 实际使用的索引,如果为null,则没有使用索引
    • 若查询中使用了覆盖索引,则该索引仅出现在key列表中
  • key_len: 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好,key_len显示的值为索引最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的

    • key_len的计算方法:
    1. varchr(10)变长字段且允许NULL : 10*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)*
    2. varchr(10)变长字段且不允许NULL : 10(Character Set:utf8=3,gbk=2,latin1=1)+2(变长字段)
    3. char(10)固定字段且允许NULL : 10*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)*
    4. char(10)固定字段且不允许NULL : 10(Character Set:utf8=3,gbk=2,latin1=1)
  • **ref:**显示索引哪一列被使用了,如果可能的话,最好是一个常数。哪些列或常量被用于查找索引列上的值

    • 由key_len可知t1表的索引idx_col1_col2被充分使用,t1表的col1匹配t2表的col1,t1表的col2匹配了一个常量,即’ac’
  • rows: 根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数

  • Extra: 包含不适合在其他列中显示但十分重要的额外信息

    • Using filesort(文件排序):MySQL中无法利用索引完成排序操作成为“文件排序”
    • Using temporary(创建临时表):使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by
    • Using index(覆盖索引):
      表示相应的select操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!
      如果同时出现using where,表明索引被用来执行索引键值的查找
      如果没有同时出现using where,表明索引用来读取数据而非执行查找动作
      • 理解方式一:就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。
      • 理解方式二:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了(或覆盖了)满足查询结果的数据就叫做覆盖索引
    • Using where:表明使用了where过滤
    • Using join buffer:表明使用了连接缓存
    • impossible where:where子句的值总是false,不能用来获取任何元组
    • 注释: distinct:优化distinct,在找到第一匹配的元组后即停止找同样值的工作

事务的四大特性

事务有四大特性(ACID )

原子性(Atomicity)

  • 原子性是指事务包含的所有操作要么全部成功,要么全部失败。成功则所有的数据库操作都生效,失败则所有的数据库操作都不生效。

一致性(Consistency)

  • 事务必须使数据库从一个一致性状态变换到另一个一致性状态。最经典的例子就是甲乙每人有100块钱,他们之间无论如何转账,总金额都应该为200元。

隔离性(Isolation)

  • 多个用户并发访问数据库时,数据库为每一个用户开启的事务。且必须不能被其他事务的操作干扰,即多个并发事务之间要相互隔离。我们常说的锁就是用于解决隔离性的一种机制,而锁的不同的粒度,使得事务有不同的隔离级别

持久性(Durability)

  • 一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的。即当系统或介质发生故障时,已提交事务的更新也不能丢失。

事务并发导致的问题

问题Problem说明
脏读Dirty reads一个事务读取了另一个事务更改但尚未提交的数据。而且在更改后被回滚了,此时第一个事务获取的数据就是无效的。我们称之为脏读。即读取到无效的垃圾数据,所以是称之为脏读
不可重复读Nonrepeatable read一个事务执行两次或两次以上的相同查询时,第二个事务在两次查询的间隔更新了数据,导致每次都得到不同的数据,即经不起一次又一次的考验,称之为不可重复。
幻读Phantom read一个事务第一次读取了M条数据,另一个并发事务插入了N条数据时。第一个事务再次读取时出现查询到了M+N条数据。好比你前一眼地球还在,一眨眼地球就不见了,很魔幻,所以称为幻读。

事务的隔离级别 *

  • Read Uncommitted(读未提交):一个事务可以读取另一个未提交事务的数据。
  • Read Committed(读已提交):一个事务要等另一个事务提交后才能读取数据。
  • Repeatable Read(可重复读):在开始读取数据(事务开启)时,不再允许修改操作。
  • Serializable(序列化):Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。

不同的隔离级别可能引发不同的一致性问题

隔离级别脏读不可重复读幻读
Read UnCommittedYYY
Read CommittedNYY
Read RepeatableNNY
serializableNNN

事务的五种状态

  1. 活动状态
    事务在执行时的状态叫活动状态。

  2. 部分提交状态
    事务中最后一条语句被执行后的状态叫部分提交状态。

  3. 失败状态
    事务不能正常执行的状态叫失败状态。

  4. 提交状态
    事务在部分提交后,将往硬盘上写入数据,当最后一条信息写入后的状态叫提交状态。进入提交状态的事务就成功完成了。

  5. 中止状态
    事务回滚并且数据库已经恢复到事务开始执行前的状态叫中止状态。

统计过慢查询么?对慢查询怎么优化?

在业务系统中除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。

慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?

所以优化也是针对这三个方向来的

  • 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
  • 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
  • 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵
    向的分表。
  • 使用mysqldumpslow日志分析工具

ACID通过什么保证的?

**A原子性:**由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

**C一致性:**由其他三大特性保证、程序代码要保证业务上的一致性

I 隔离性: 由MVCC来保证

D持久性: 由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从red log恢复

InnoDB redo log写盘,InnoDB事务进入 prepare【准备】 状态。
如果前面 prepare成功,binlog写盘,再继续将事务日志持久化到 binlog,如果持久化成功,那么、InnoDB事务则进入commit状态(在 redo log里面写一个commit记录)

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

什么是 MVCC ?

全称 Multi-Version Concurrency Control,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

MVCCMySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

什么是当前读和快照读?

在学习 MVCC 多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的当前读和快照读?

  • 当前读:像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读:像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

MVCC 就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。

MVCC解决了那些问题?

  • 并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作
  • 解决脏读,幻读,不可重复读,等事务耿立问题,但不能解决写-写,更新丢失问题。

MVCC的实现原理

每行数据的隐藏字段 db_trx_id,db_roll_pointer,db_row_id, 和 read view来实现的。

每行记录除了我们自定义的字段外,还有数据库隐式定义的 db_trx_id, db_roll_ptr,db_rowl_id 等字段

  • db_trx_id:6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
  • db_roll_ptr:7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)
  • db_rowl_id:6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引
    实际还有一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了

当前读,快照读和MVCC的关系

  • MVCC 多版本并发控制是 「维持一个数据的多个版本,使得读写操作没有冲突」 的概念,只是一个抽象概念,并非实现
  • 因为 MVCC 只是一个抽象概念,要实现这么一个概念,MySQL 就需要提供具体的功能去实现它,「快照读就是 MySQL 实现 MVCC 理想模型的其中一个非阻塞读功能」。而相对而言,当前读就是悲观锁的具体功能实现
  • 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC 模型在 MySQL 中的具体实现则是由 3 个隐式字段,undo 日志 ,Read View 等去完成的,具体可以看下面的 MVCC 实现原理

MVCC 带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

什么是 Read View?

什么是 Read View,说白了 Read View 就是事务进行快照读操作的时候生产的读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID (当每个事务开启时,都会被分配一个 ID , 这个 ID 是递增的,所以最新的事务,ID 值越大)

所以我们知道 Read View 主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View 的几个属性:

  • m_ids: 当前系统活跃(未提交)事务版本号集合
  • min_trx_id: 表示当前生成Read View 当前系统活跃的读写事务中最小的事务id,也就是 m_ids 中最小的id值
  • max_trx_id: 表示当前生成 Read View 时系统应该分配的下一个事务id值
  • creator_txr_id: 表示生成Read View 的事务id值

Read View 如何判断版本链中的那个版本可用?

  • trx_id == creator_txr_id: 可以访问,当前的Read View 是他自己生成的当然可以自己进行访问
  • trx_id<min_trx_id : 可以访问,当前的事务id小于了当前活跃事务id的最小值,说明他已经被提交了
  • trx_id>max_trx_id: 不可以访问,当前的事务id大于了下一个即将分配的事务id
  • min_trx_id<=trx_id<=max_trx_id:如果trx_id在m_ids中,是不可以访问这个版本的,在m_ids说明是未提交的,不可以访问,反之则可以。

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的 db_trx_id(即当前事务 ID )取出来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果 db_trx_id 跟 Read View 的属性做了某些比较,不符合可见性,那就通过db_roll_ptr 回滚指针去取出 Undo Log 中的 db_trx_id 再比较,即遍历链表的 db_trx_id(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的 db_trx_id, 那么这个 db_trx_id 所在的旧记录就是当前事务能看见的最新老版本

mysql主从同步原理

mysql主从同步原理mysql主从同步的过程:

Mysal的主从复制中主要有三个线程: master (binlog dump thread) , slave (I/o thread ,SQLthread), Master—条线程和Slave中的两条线程。

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

注:主从节点使用binglog文件+ position偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机重启,则会自动从position的位置发起同步。

由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。

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

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

简述MylSAM和InnoDB的区别

MylSAM:

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

lnnoDb:

  • 支持ACID的事务,支持事务的四种隔离级别;
  • 支持行级锁及外键约束,因此可以支持写并发;
  • 不存储总行数;
  • 一个InnoDB引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;

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

普通索引: 允许被索引的数据列包含重复的值。

唯一索引: 可以保证数据记录的唯一性。

主键索引: 是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,用关键字primary key来创建。

联合索引: 索引可以覆盖多个数据列,如像INDEX(columnA, columnB)索引。

全文索引: 通过建立倒排索引,可以极大的提升检索效率;解决判断字段是否包含的问题,是目前搜索引擎使
用的一种关键技术。可以通过ALTERTABLE table_name ADD FULLTEXT (column);创建全文索引
索引可以极大的提高数据的查询速度。

通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

但是会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件

索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大,如果非聚集索引很多,一旦聚集索引改变,那么所有非聚集索引都会跟着变。

Redis

简单讲讲Redis的含义

①Redis (REmote DIctionary Server) 是用 C 语言开发的一个开源的高性能键值对(key-value)数据库

②Redis数据库中的数据间没有必然的关联关系,内部采用单线程机制进行工作,性能比较高,支持持久化存储

③支持多种数据类型,包括字符串类型 string、列表类型 list、散列类型 hash、集合类型 set、有序集合类型 sorted_set

④他是一个基于内存的缓存数据库,效率上比MySQL高,一般用来做热点数据缓存

redis key存储的最大长度

key最大长度存放 512MB

Redis最适合的场景?

缓存

  • 将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率;

会话缓存(Session Cache)

  • 最常用的一种使用 Redis 的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗? 幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台Magento 也提供 Redis 的插件;
  • 可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性;

全页缓存(FPC)

  • 除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进。 再次以Magento为例,Magento提供一个插件来使用 Redis作为全页缓存后端。 此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面;

队列

  • Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop操作。 如果你快速的在 Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看;

排行榜

  • Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用Ruby实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到;

发布/订阅(不常用)

  • Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统;

分布式锁实现

  • 在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现;

计数器

  • 可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量;

RDB和AOF

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

优点:

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

Redis键过期删除策略

redis使用一个过期字典来保存数据库所有键的过期时间,过期字典的key是一个指针,指向redis的键,value则存储一个毫秒精度的UNIX时间戳。

现有以下三种可能的过期键删除策略:

  1. 定时删除:在设置键过期时间的同时,创建一个定时器,让定时器在键过期时,立即删除键;对内存友好而对CPU不友好;
  2. 惰性删除:只有用到的时候才会判断是否删除;对CPU友好但对内存不友好,如果一个过期键永远不被访问,那该键占用的内存也就永远得不到释放;
  3. 定期删除:每隔一段时间,就检查一次;折中方案,需要合理设置删除操作的执行时间和执行频率;

redis实际使用的是惰性删除和定期删除两种。

redis对于惰性删除的做法是:

  • 每次访问键之前需要判断键是否过期;

redis对于定期删除的做法是:

  • 默认每隔100ms检查是否有过期的key,有则删除。需要说明的是,redis不是每隔100ms将所有的key检查一次,而是随机抽取进行检查;

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

Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器file event handler。这个文件事件处理器,它是单线程的,所以Redis才叫做单线程的模型,它采用O多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis 内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个Socket、lO多路复用程序、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)。

多个Socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个
Socket,会将Socket放入一个队列中排队,每次从队列中取出一个Socket给事件分派器,事件分派器把Socket给对应的事件处理器。
然后一个Socket的事件处理完之后,IO多路复用程序才会将队列中的下一个Socket给事件分派器。文件事件分派器会根据每个Socket当前产生的事件,来选择对应的事件处理器来处理。

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OtlkUOO7-1649420221292)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5CaHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9PcVRBbDNXVEM3RmsyQmxOc3MzcmliRlA4NklWak5yTmc3VkVOODZWNVpsRHppYjlOODlib1BQQTZPSjd4NzFPcGljU1hlTXZPbHEyMGFzUXZWaWFOQTU1UHcvNjQwP3d4X2ZtdD1wbmc)]

单线程快的原因:

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

Redis缓存异常

*缓存雪崩: **

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  • 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

缓存穿透:*

缓存穿透是指缓存和数据库中都没有的数据 ,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

附加

对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
Bitmap: 典型的就是哈希表
缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

布隆过滤器(推荐)

就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。
Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

**缓存击穿: * **

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  • 设置热点数据永远不过期。
  • 加互斥锁,互斥锁

缓存预热:

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案

  • 直接写个缓存刷新页面,上线时手工操作一下;
  • 数据量不大,可以在项目启动的时候自动进行加载;
  • 定时刷新缓存;

缓存降级:

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
热点数据和冷数据

热点数据,缓存才有价值

对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存

对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

缓存热点key:

缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

缓存穿透和缓存击穿的区别

  • 缓存穿透是指缓存和数据库都不存在的数据,导致请求全部都落在数据库上
  • 缓存击穿是指缓存中没有但数据库中有的数据此时由于并发用户多,导致没有从缓存中读取到数据,直接去数据库取

Redis的内存淘汰策略有哪些

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis的内存用完了会发生什么?

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面

Redis key的过期时间和永久有效分别怎么设置?

EXPIRE和PERSIST命令。
我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  • 定时去清理过期的缓存;

  • 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

为什么要用 Redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

Redis有哪些数据类型

Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分的使用要求

数据类型可以存储的值操作应用场景
STRING字符串、整数或者浮点数对整个字符串或者字符串的其中一部分执行操作,对整数和浮点数执行自增或者自减操作做简单的键值对缓存
LIST列表从两端压入或者弹出元素,对单个或者多个元素进行修剪,,只保留一个范围内的元素存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据
SET无序集合添加、获取、移除单个元素,检查一个元素是否存在于集合中, 计算交集、并集、差集,从集合里面随机获取元素交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集
HASH包含键值对的无序散列表添加、获取、移除单个键值对获取所有键值对检查某个键是否存在 结构化的数据,比如一个对象
ZSET有序集合添加、获取、删除元素根据分值范围或者成员来获取元素计算一个键的排名 去重但可以排序,如获取排名前几名的用户

Redis事务

什么是事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis事务的概念

Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务的三个阶段

  • 事务开始 MULTI
  • 命令入队
  • 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

Redis事务相关命令

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的

Redis会将一个事务中的所有命令序列化,然后按顺序执行。

  1. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速
  2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
  3. 如果在一个事务中出现运行错误,那么正确的命令会被执行。
  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 null。
    通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
  • UNWATCH命令可以取消watch对所有key的监控。

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

Redis集群会有写操作丢失吗?为什么?

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

写操作会丢失的情况:

  • 过期 key 被清理
  • 最大内存不足,导致 Redis 自动清理部分 key 以节省空间
  • 主库故障后自动重启,从库自动同步
  • 单独的主备方案,网络不稳定触发哨兵的自动切换主从节点,切换期间会有数据丢失

Redis集群之间是如何复制的?

异步复制

Redis集群最大节点个数是多少?

16384个

Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

Redis 哨兵模式

Redis的主从复制模式下, 一旦主节点由于故障不能提供服务, 需要人工将从节点晋升为主节点, 同时还要通知应用方更新主节点地址, 对于很多应用场景这种故障处理的方式是无法接受的。 可喜的是Redis从2.8开始正式提供了Redis Sentinel(哨兵) 架构来解决这个问题。

Redis主从复制的缺点:没有办法对master进行动态选举,需要使用Sentinel机制完成动态选举

哨兵进程的作用

  1. 监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。
  2. 提醒(Notification):当被监控的某个Redis节点出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。
  3. 自动故障迁移(Automatic failover):当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作。
    1. 它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master;
    2. 当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用现在的Master替换失效Master。
    3. Master和Slave服务器切换后,Master的redis.conf、Slave的redis.conf和sentinel.conf的配置文件的内容都会发生相应的改变,即,Master主服务器的redis.conf配置文件中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换

哨兵进程的工作方式

  1. 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
  2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值,则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)。
  3. 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态。
  4. 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)。
  5. 在一般情况下, 每个Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
  6. 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  7. 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

Redis实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。

当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

使用SETNX完成同步锁的流程及事项如下:

使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间

释放锁,使用DEL命令将锁数据删除

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用Redis做过异步队列吗,是如何实现的

使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
Redis如何实现延时队列

使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。

Redis回收进程如何工作的?

  1. 一个客户端运行了新的命令,添加了新的数据。
  2. Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
  3. 一个新的命令被执行,等等。
  4. 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

git常用命令

查看文件状态: git status

将文件添加到缓存区:

  • 添加指定文件到缓存区:git add fileName
  • 添加多个文件到缓存区:git add fileName1 fileName2
  • 将当工作区下的所有文件添加到缓存区: git add . git add --all
  • 指定某个后缀名提交到缓存区: git add *.xml (将当前目录下所有.xml文件提交到工作区)

取消添加到暂存区:

  • 会删除该目录下的所有文件和子目录: git rm -r *
  • 删除指定文件,包含本地目录和index中的此文件记录:git rm fileName
  • 将指定文件从缓存区中移除:git rm --ached fileName

将文件添加到本地仓库: git的提交注释必须不能为空,否则就会提交失败。

  • 将暂存区指定文件添加到本地仓库: git commit -m “提交的注释信息” fileName
  • 将指定被git跟踪的文件添加到本地仓库(将被git跟踪过的文件强制提交到本地仓库): git commit -am fileName

查看历史版本记录:

  • 查看提交到本地仓库的历史记录: git log
  • git log --pretty=oneline
  • git log --oneline
  • git reflog (推荐使用这个比较好)

查看文件差异:

  • 显示暂存区和工作区的差异: git diff fileName (如果不跟文件名比较的就是全部)
  • 显示暂存区和上一次commit的差异: git diff --cached fileName
  • 比较两个分支最后一次commit的差异: git diff branchName1 branchName2
  • 显示工作目录与最后一次commit之间的文件变更差异:git diff HEAD

版本的前进和后退:

  • 基于索引值操作 【推荐】: git reset --hard [局部索引值] Eg: git reset --hard 969731b
  • 使用^符号:只能后退 (一个^表示后退一步,n 个表示后退 n 步 ):git reset --hard HEAD^
  • 使用~符号:只能后退(表示后退 n 步) : git reset --hard HEAD~n Eg 后台2步: git reset --hard HEAD~2

**标签管理:**比如开发到某个重要的版本可以给该项目打上一个标签

  • git tag -a [tagname] -m “标签信息”: 可以指定标签信息;
  • git tag :可以查看所有标签。
  • git log --pretty=oneline --abbrev-commit :找到历史提交的commit id
  • git tag -d [tagname] : 删除标签
  • git push origin :refs/tags/tagname : 可以删除一个远程标签。

reset 命令的三个参数对比:

  • –soft 参数
    • 仅仅在本地库移动 HEAD 指针
    • 暂存区和工作区没有修改
  • –mixed 参数
    • 在本地库移动 HEAD 指针
    • 重置暂存区 ,使其与本地库一致
  • –hard 参数 [常用]
    • 在本地库移动 HEAD 指针
    • 重置暂存区,使其与本地库一致
    • 重置工作区,使其与本地库一致

分支开发:

  • 分支开发的好处:

    • 同时并行推进多个功能开发,提高开发效率 。
    • 各个分支在开发过程中,如果某一个分支开发失败,不会对其他分支有任何影响。失败的分支删除重新开始即可。
  • 新建分支:git branch [branchName]

  • 切换分支:git checkout [branchname]

  • 查看分支(前面代 * 号表示当前所在分支) : git branch -v (查看当前所有分支)git branch -a (查看当前所有分支包括远程分支)

  • 合并分支:

    • 切换到接受修改的分支(被合并,增加新内容)上 :git checkout [被合并分支名]
    • 执行 merge 命令 :git merge [有新内容分支名]
  • 分支切换问题:假设我在dev分支上新建文件 demo.txt,然后切换到pro分支,发现pro分支也有demo.txt文件(分支之间相互影响)

    • 解决方法1:在dev分支上线 git add,然后在git commit,在切换到pro分支就不会出现这种问题
  • 如果当前分支工作还没有做完,又想去其他分支,用 git stash save “注释信息” 隐藏当前工作现场,此时用 git status当前分支工作区是干净的,可以放心去其他的分支,在其他分支处理完后回到当前分支,git stash pop将存储到栈中未提价的文件恢复,使用git stash list可以查看 stash中的内容

    • git stash apply:将堆栈中的内容应用到当前目录,不同于git stash pop,该命令不会将内容从堆栈中删除,也就说该命令能够将堆栈的内容多次应用到工作目录中,适应于多个分支的情况。可以使用git stash apply + stash名字(如stash@{2})指定恢复哪个stash到当前的工作目录。
  • git stash show stash@{1} :查看指定的stash和当前目录差异。

    • git stash show -p : 查看stash和当前目录差异详细信息。
    • git stash branch:从最新的stash创建分支。
  • 删除分支:

    • git branch -d [branchname]: 会在删除前检查merge状态
    • git branch -D :直接删除该分支
  • 分支冲突解决:如果多个分支修改了同一行内容,在进行合并时就会发生冲突

    • 解决的办法:
      • 将文件内容中的提示的符号去掉,修改为我们想要的内容
      • 将文件 git add 添加到缓冲区,然后使用 gti commit -m “提交信息” 进行提交即可,不需要跟文件名

linux常用命令

开关机命令

    1、shutdown –h now:立刻进行关机

    2、shutdown –r now:现在重新启动计算机

    3、reboot:现在重新启动计算机

    4、su -:切换用户;passwd:修改用户密码

    5、logout:用户注销

常用快捷命令

    1、tab = 补全

    2、ctrl + l -:清屏,类似clear命令

    3、ctrl + r -:查找历史命令(history);ctrl+c = 终止

    4、ctrl+k = 删除此处至末尾所有内容

    5、ctrl+u = 删除此处至开始所有内容

wc:文本统计统计
wordcount
3 5 29 a.txt
行数 单词数 字符数 文件名
常见参数:
-l:只查看行数
-w: 只查看单词数
-c:只查看字符数

du:文件大小统计
格式:du [选项参数] dir_path
常见参数:
-s:只统计该文件目录的大小,不递归
-h:人性化的显示单位

find:文件检索命令

语法
 
find   path   -option   [   -print ]   [ -exec   -ok   command ]   {} \;
 
参数说明 :
 
find 根据下列规则判断 path 和 expression,在命令列上第一个 - ( ) , ! 之前的部份为 path,之后的是 expression。如果 path 是空字串则使用目前路径,如果 expression 是空字串则使用 -print 为预设 expression。
 
expression 中可使用的选项有二三十个之多,在此只介绍最常用的部份。
 
-mount, -xdev : 只检查和指定目录在同一个文件系统下的文件,避免列出其它文件系统中的文件
 
-amin n : 在过去 n 分钟内被读取过
 
-anewer file : 比文件 file 更晚被读取过的文件
 
-atime n : 在过去n天内被读取过的文件
 
-cmin n : 在过去 n 分钟内被修改过
 
-cnewer file :比文件 file 更新的文件
 
-ctime n : 在过去n天内被修改过的文件
 
-empty : 空的文件-gid n or -group name : gid 是 n 或是 group 名称是 name
 
-ipath p, -path p : 路径名称符合 p 的文件,ipath 会忽略大小写
 
-name name, -iname name : 文件名称符合 name 的文件。iname 会忽略大小写
 
-size n : 文件大小 是 n 单位,b 代表 512 位元组的区块,c 表示字元数,k 表示 kilo bytes,w 是二个位元组。-type c : 文件类型是 c 的文件。
 
d: 目录
 
c: 字型装置文件
 
b: 区块装置文件
 
p: 具名贮列
 
f: 一般文件
 
l: 符号连结
 
s: socket
 
-pid n : process id 是 n 的文件
 
你可以使用 ( ) 将运算式分隔,并使用下列运算。
 
exp1 -and exp2
 
! expr
 
-not expr
 
exp1 -or exp2
 
exp1, exp2
实例
 
将目前目录及其子目录下所有延伸档名是 c 的文件列出来。
 
# find . -name "*.c"
 
将目前目录其其下子目录中所有一般文件列出
 
# find . -type f
 
将目前目录及其子目录下所有最近 20 天内更新过的文件列出
 
# find . -ctime -20
 
查找/var/log目录中更改时间在7日以前的普通文件,并在删除之前询问它们:
 
# find /var/log -type f -mtime +7 -ok rm {} \;
 
查找前目录中文件属主具有读、写权限,并且文件所属组的用户和其他用户具有读权限的文件:
 
# find . -type f -perm 644 -exec ls -l {} \;
 
为了查找系统中所有文件长度为0的普通文件,并列出它们的完整路径:
 
# find / -type f -size 0 -exec ls -l {} \;

常用目录/文件操作命令

1.展示目录列表命令ls(list)

ls 展示当前目录下的可见文件
ls -a 展示当前目录下所有的文件(包括隐藏的文件)
ls -l(ll) 展示当前目录下文件的详细信息
ll -a 展示当前目录下所有文件的详细信息
ll -h 友好的显示当前目录下文件的详细信息(其实就是文件的大小可读性更强了)

2.切换目录命令cd(change directory)

cd test 切换到test目录下
cd … 切换到上一级目录
cd / 切换到系统根目录下
cd ~ 切换到当前用户的根目录下
cd - 切换到上一级所在的目录
pwd:显示目前的目录

3.目录的创建(mkdir)和删除(rmdir)命令

mkdir test 在当前目录下创建一个test目录
mkdir -p test/a/b 在test目录下的a目录下创建一个b目录,如果上一级目录不存在,则连它的父目录一起创建
rmdir test 删除当前目录下的test目录(注意:该命令只能够删除空目录)

4.文件的创建(touch)和删除(rm)命令

touch test.txt 在当前目录下创建一个test.txt的文件
rm test.txt 删除test.txt的文件(带询问的删除,需输入y才能删除)
rm -f test.txt 直接删除text.txt文件
rm -r test 递归删除,即删除test目录以及其目录下的子目录(带询问的删除)
rm -rf

5.文件复制(cp)
选项与参数:

-a:相当於 -pdr 的意思,至於 pdr 请参考下列说明;(常用)

-d:若来源档为连结档的属性(link file),则复制连结档属性而非文件本身;

-f:为强制(force)的意思,若目标文件已经存在且无法开启,则移除后再尝试一次;

-i:若目标档(destination)已经存在时,在覆盖时会先询问动作的进行(常用)

-l:进行硬式连结(hard link)的连结档创建,而非复制文件本身;

-p:连同文件的属性一起复制过去,而非使用默认属性(备份常用);

-r:递归持续复制,用於目录的复制行为;(常用)

-s:复制成为符号连结档 (symbolic link),亦即『捷径』文件;

-u:若 destination 比 source 旧才升级 destination !test 直接删除test目录以及其目录下的子目录

6.rm (移除文件或目录)
选项与参数:

-f :就是 force 的意思,忽略不存在的文件,不会出现警告信息;
-i :互动模式,在删除前会询问使用者是否动作
-r :递归删除啊!最常用在目录的删除了!这是非常危险的选项!!!

7.mv (移动文件与目录,或修改名称)
选项与参数:

-f :force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖;
-i :若目标文件 (destination) 已经存在时,就会询问是否覆盖!
-u :若目标文件已经存在,且 source 比较新,才会升级 (update)

8.查看文件内容

cat  由第一行开始显示文件内容
tac  从最后一行开始显示,可以看出 tac 是 cat 的倒著写!
nl   显示的时候,顺道输出行号!
more 一页一页的显示文件内容
less 与 more 类似,但是比 more 更好的是,他可以往前翻页!
head 只看头几行
tail 只看尾部几行

9.文件系统权限

文件的权限 - 使用 “+” 设置权限,使用 “-” 用于取消
ls -lh 显示权限
ls /tmp | pr -T5 -W$COLUMNS 将终端划分成5栏显示
chmod ugo+rwx directory1 设置目录的所有人(u)、群组(g)以及其他人(o)以读(r )、写(w)和执行(x)的权限
chmod go-rwx directory1 删除群组(g)与其他人(o)对目录的读写执行权限
chown user1 file1 改变一个文件的所有人属性
chown -R user1 directory1 改变一个目录的所有人属性并同时改变改目录下所有文件的属性
chgrp group1 file1 改变文件的群组
chown user1:group1 file1 改变一个文件的所有人和群组属性
find / -perm -u+s 列出一个系统中所有使用了SUID控制的文件
chmod u+s /bin/file1 设置一个二进制文件的 SUID 位 - 运行该文件的用户也被赋予和所有者同样的权限
chmod u-s /bin/file1 禁用一个二进制文件的 SUID位
chmod g+s /home/public 设置一个目录的SGID 位 - 类似SUID ,不过这是针对目录的
chmod g-s /home/public 禁用一个目录的 SGID 位
chmod o+t /home/public 设置一个文件的 STIKY 位 - 只允许合法所有人删除文件
chmod o-t /home/public 禁用一个目录的 STIKY 位

10.用户和群组
groupadd group_name 创建一个新用户组
groupdel group_name 删除一个用户组
groupmod -n new_group_name old_group_name 重命名一个用户组
useradd -c "Name Surname " -g admin -d /home/user1 -s /bin/bash user1 创建一个属于 “admin” 用户组的用户
useradd user1 创建一个新用户
userdel -r user1 删除一个用户 ( ‘-r’ 排除主目录)
usermod -c “User FTP” -g system -d /ftp/user1 -s /bin/nologin user1 修改用户属性
passwd 修改口令
passwd user1 修改一个用户的口令 (只允许root执行)
chage -E 2005-12-31 user1 设置用户口令的失效期限
pwck 检查 ‘/etc/passwd’ 的文件格式和语法修正以及存在的用户
grpck 检查 ‘/etc/passwd’ 的文件格式和语法修正以及存在的群组
newgrp group_name 登陆进一个新的群组以改变新创建文件的预设群组

11.打包和压缩文件
bunzip2 file1.bz2 解压一个叫做 'file1.bz2’的文件
bzip2 file1 压缩一个叫做 ‘file1’ 的文件
gunzip file1.gz 解压一个叫做 'file1.gz’的文件
gzip file1 压缩一个叫做 'file1’的文件
gzip -9 file1 最大程度压缩
rar a file1.rar test_file 创建一个叫做 ‘file1.rar’ 的包
rar a file1.rar file1 file2 dir1 同时压缩 ‘file1’, ‘file2’ 以及目录 ‘dir1’
rar x file1.rar 解压rar包
unrar x file1.rar 解压rar包
tar -cvf archive.tar file1 创建一个非压缩的 tarball
tar -cvf archive.tar file1 file2 dir1 创建一个包含了 ‘file1’, ‘file2’ 以及 'dir1’的档案文件
tar -tf archive.tar 显示一个包中的内容
tar -xvf archive.tar 释放一个包
tar -xvf archive.tar -C /tmp 将压缩包释放到 /tmp目录下
tar -cvfj archive.tar.bz2 dir1 创建一个bzip2格式的压缩包
tar -xvfj archive.tar.bz2 解压一个bzip2格式的压缩包
tar -cvfz archive.tar.gz dir1 创建一个gzip格式的压缩包
tar -xvfz archive.tar.gz 解压一个gzip格式的压缩包
zip file1.zip file1 创建一个zip格式的压缩包
zip -r file1.zip file1 file2 dir1 将几个文件和目录同时压缩成一个zip格式的压缩包
unzip file1.zip 解压一个zip格式压缩包

12.RPM 包 - (Fedora, Redhat及类似系统)
rpm -ivh package.rpm 安装一个rpm包
rpm -ivh --nodeeps package.rpm 安装一个rpm包而忽略依赖关系警告
rpm -U package.rpm 更新一个rpm包但不改变其配置文件
rpm -F package.rpm 更新一个确定已经安装的rpm包
rpm -e package_name.rpm 删除一个rpm包
rpm -qa 显示系统中所有已经安装的rpm包
rpm -qa | grep httpd 显示所有名称中包含 “httpd” 字样的rpm包
rpm -qi package_name 获取一个已安装包的特殊信息
rpm -qg “System Environment/Daemons” 显示一个组件的rpm包
rpm -ql package_name 显示一个已经安装的rpm包提供的文件列表
rpm -qc package_name 显示一个已经安装的rpm包提供的配置文件列表
rpm -q package_name --whatrequires 显示与一个rpm包存在依赖关系的列表
rpm -q package_name --whatprovides 显示一个rpm包所占的体积
rpm -q package_name --scripts 显示在安装/删除期间所执行的脚本l
rpm -q package_name --changelog 显示一个rpm包的修改历史
rpm -qf /etc/httpd/conf/httpd.conf 确认所给的文件由哪个rpm包所提供
rpm -qp package.rpm -l 显示由一个尚未安装的rpm包提供的文件列表
rpm --import /media/cdrom/RPM-GPG-KEY 导入公钥数字证书
rpm --checksig package.rpm 确认一个rpm包的完整性
rpm -qa gpg-pubkey 确认已安装的所有rpm包的完整性
rpm -V package_name 检查文件尺寸、 许可、类型、所有者、群组、MD5检查以及最后修改时间
rpm -Va 检查系统中所有已安装的rpm包- 小心使用
rpm -Vp package.rpm 确认一个rpm包还未安装
rpm2cpio package.rpm | cpio --extract --make-directories bin 从一个rpm包运行可执行文件
rpm -ivh /usr/src/redhat/RPMS/arch/package.rpm 从一个rpm源码安装一个构建好的包
rpmbuild --rebuild package_name.src.rpm 从一个rpm源码构建一个 rpm 包

13.YUM 软件包升级器 - (Fedora, RedHat及类似系统)

yum install package_name 下载并安装一个rpm包
yum localinstall package_name.rpm 将安装一个rpm包,使用你自己的软件仓库为你解决所有依赖关系
yum update package_name.rpm 更新当前系统中所有安装的rpm包
yum update package_name 更新一个rpm包
yum remove package_name 删除一个rpm包
yum list 列出当前系统中安装的所有包
yum search package_name 在rpm仓库中搜寻软件包
yum clean packages 清理rpm缓存删除下载的包
yum clean headers 删除所有头文件
yum clean all 删除所有缓存的包和头文件

font>
选项与参数:

-f :force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖;
-i :若目标文件 (destination) 已经存在时,就会询问是否覆盖!
-u :若目标文件已经存在,且 source 比较新,才会升级 (update)

8.查看文件内容

cat  由第一行开始显示文件内容
tac  从最后一行开始显示,可以看出 tac 是 cat 的倒著写!
nl   显示的时候,顺道输出行号!
more 一页一页的显示文件内容
less 与 more 类似,但是比 more 更好的是,他可以往前翻页!
head 只看头几行
tail 只看尾部几行

9.文件系统权限

文件的权限 - 使用 “+” 设置权限,使用 “-” 用于取消
ls -lh 显示权限
ls /tmp | pr -T5 -W$COLUMNS 将终端划分成5栏显示
chmod ugo+rwx directory1 设置目录的所有人(u)、群组(g)以及其他人(o)以读(r )、写(w)和执行(x)的权限
chmod go-rwx directory1 删除群组(g)与其他人(o)对目录的读写执行权限
chown user1 file1 改变一个文件的所有人属性
chown -R user1 directory1 改变一个目录的所有人属性并同时改变改目录下所有文件的属性
chgrp group1 file1 改变文件的群组
chown user1:group1 file1 改变一个文件的所有人和群组属性
find / -perm -u+s 列出一个系统中所有使用了SUID控制的文件
chmod u+s /bin/file1 设置一个二进制文件的 SUID 位 - 运行该文件的用户也被赋予和所有者同样的权限
chmod u-s /bin/file1 禁用一个二进制文件的 SUID位
chmod g+s /home/public 设置一个目录的SGID 位 - 类似SUID ,不过这是针对目录的
chmod g-s /home/public 禁用一个目录的 SGID 位
chmod o+t /home/public 设置一个文件的 STIKY 位 - 只允许合法所有人删除文件
chmod o-t /home/public 禁用一个目录的 STIKY 位

10.用户和群组
groupadd group_name 创建一个新用户组
groupdel group_name 删除一个用户组
groupmod -n new_group_name old_group_name 重命名一个用户组
useradd -c "Name Surname " -g admin -d /home/user1 -s /bin/bash user1 创建一个属于 “admin” 用户组的用户
useradd user1 创建一个新用户
userdel -r user1 删除一个用户 ( ‘-r’ 排除主目录)
usermod -c “User FTP” -g system -d /ftp/user1 -s /bin/nologin user1 修改用户属性
passwd 修改口令
passwd user1 修改一个用户的口令 (只允许root执行)
chage -E 2005-12-31 user1 设置用户口令的失效期限
pwck 检查 ‘/etc/passwd’ 的文件格式和语法修正以及存在的用户
grpck 检查 ‘/etc/passwd’ 的文件格式和语法修正以及存在的群组
newgrp group_name 登陆进一个新的群组以改变新创建文件的预设群组

11.打包和压缩文件
bunzip2 file1.bz2 解压一个叫做 'file1.bz2’的文件
bzip2 file1 压缩一个叫做 ‘file1’ 的文件
gunzip file1.gz 解压一个叫做 'file1.gz’的文件
gzip file1 压缩一个叫做 'file1’的文件
gzip -9 file1 最大程度压缩
rar a file1.rar test_file 创建一个叫做 ‘file1.rar’ 的包
rar a file1.rar file1 file2 dir1 同时压缩 ‘file1’, ‘file2’ 以及目录 ‘dir1’
rar x file1.rar 解压rar包
unrar x file1.rar 解压rar包
tar -cvf archive.tar file1 创建一个非压缩的 tarball
tar -cvf archive.tar file1 file2 dir1 创建一个包含了 ‘file1’, ‘file2’ 以及 'dir1’的档案文件
tar -tf archive.tar 显示一个包中的内容
tar -xvf archive.tar 释放一个包
tar -xvf archive.tar -C /tmp 将压缩包释放到 /tmp目录下
tar -cvfj archive.tar.bz2 dir1 创建一个bzip2格式的压缩包
tar -xvfj archive.tar.bz2 解压一个bzip2格式的压缩包
tar -cvfz archive.tar.gz dir1 创建一个gzip格式的压缩包
tar -xvfz archive.tar.gz 解压一个gzip格式的压缩包
zip file1.zip file1 创建一个zip格式的压缩包
zip -r file1.zip file1 file2 dir1 将几个文件和目录同时压缩成一个zip格式的压缩包
unzip file1.zip 解压一个zip格式压缩包

12.RPM 包 - (Fedora, Redhat及类似系统)
rpm -ivh package.rpm 安装一个rpm包
rpm -ivh --nodeeps package.rpm 安装一个rpm包而忽略依赖关系警告
rpm -U package.rpm 更新一个rpm包但不改变其配置文件
rpm -F package.rpm 更新一个确定已经安装的rpm包
rpm -e package_name.rpm 删除一个rpm包
rpm -qa 显示系统中所有已经安装的rpm包
rpm -qa | grep httpd 显示所有名称中包含 “httpd” 字样的rpm包
rpm -qi package_name 获取一个已安装包的特殊信息
rpm -qg “System Environment/Daemons” 显示一个组件的rpm包
rpm -ql package_name 显示一个已经安装的rpm包提供的文件列表
rpm -qc package_name 显示一个已经安装的rpm包提供的配置文件列表
rpm -q package_name --whatrequires 显示与一个rpm包存在依赖关系的列表
rpm -q package_name --whatprovides 显示一个rpm包所占的体积
rpm -q package_name --scripts 显示在安装/删除期间所执行的脚本l
rpm -q package_name --changelog 显示一个rpm包的修改历史
rpm -qf /etc/httpd/conf/httpd.conf 确认所给的文件由哪个rpm包所提供
rpm -qp package.rpm -l 显示由一个尚未安装的rpm包提供的文件列表
rpm --import /media/cdrom/RPM-GPG-KEY 导入公钥数字证书
rpm --checksig package.rpm 确认一个rpm包的完整性
rpm -qa gpg-pubkey 确认已安装的所有rpm包的完整性
rpm -V package_name 检查文件尺寸、 许可、类型、所有者、群组、MD5检查以及最后修改时间
rpm -Va 检查系统中所有已安装的rpm包- 小心使用
rpm -Vp package.rpm 确认一个rpm包还未安装
rpm2cpio package.rpm | cpio --extract --make-directories bin 从一个rpm包运行可执行文件
rpm -ivh /usr/src/redhat/RPMS/arch/package.rpm 从一个rpm源码安装一个构建好的包
rpmbuild --rebuild package_name.src.rpm 从一个rpm源码构建一个 rpm 包

13.YUM 软件包升级器 - (Fedora, RedHat及类似系统)

yum install package_name 下载并安装一个rpm包
yum localinstall package_name.rpm 将安装一个rpm包,使用你自己的软件仓库为你解决所有依赖关系
yum update package_name.rpm 更新当前系统中所有安装的rpm包
yum update package_name 更新一个rpm包
yum remove package_name 删除一个rpm包
yum list 列出当前系统中安装的所有包
yum search package_name 在rpm仓库中搜寻软件包
yum clean packages 清理rpm缓存删除下载的包
yum clean headers 删除所有头文件
yum clean all 删除所有缓存的包和头文件

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值