总结个人版

一、java基础

1、面向对象

什么是面向对象?
对比面向过程,是两种不同的处理问题的角度
面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么
比如:洗衣机洗衣服
面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机----->2、放衣服----->3、放洗衣粉-----
>4、清洗----->5、烘干
面向对象会拆出人和洗衣机两个对象:
人:打开洗衣机 放衣服 放洗衣粉
洗衣机:清洗 烘干
从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护

面向对象
封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项
内部细节对外部调用透明,外部调用无需修改或者关心内部实现
1、javabean的属性私有,提供getset对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决
定。而不能由外部胡乱修改

private String name;
public void setName(String name){
this.name = "tuling_"+name;
} 该name有自己的命名规则,明显不能由外部直接赋值

2、orm框架
操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入mybatis,调方法即可

继承:继承基类的方法,并做出自己的改变和/或扩展
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的

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

父类类型 变量名 = new 子类对象 ;
变量名.方法名();

弊端:无法调用子类特有的功能

2、Java语言有哪些特点

  1. 安全、可靠
  2. 平台无关性(一次编译,到处运行)
  3. 支持多线程
  4. 面向对象
  5. 编译与解释并存
  6. 简单易学

3、java与C++的区别

相同:

  • 都是面向对象的语言,都具有继承、封装和多态的特点

不同:

  • Java没有指针相关操作,比c++相对简单安全;
  • java对内存有自动管理机制,而c++需要手动进行关闭清理
  • Java只能单继承,而c++能够多继承

4、JDK JRE JVM

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NUiiYRo2-1637023656088)(C:\Users\xiaoting\AppData\Roaming\Typora\typora-user-images\image-20211101193536931.png)]

5、字符型常量和字符串常量的区别

形式上:

  • 字符型常量用单引号,字符串常量用双引号

内存上:

  • 字符型常量占用两个字节,字符串常量占多个字节码,随长度而变

计算上:

  • 字符型常量可以看作一个整型ASCAII码,直接参与运算,而字符串常量不行

6、自动装箱和拆箱

装箱:基本类型转换相应的引用类型的过程叫装箱

int - > Integer char - > Character 剩下的都是首字母写成大写

拆箱:引用类型转换基本类型的过程

7、==和equals比较

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

public boolean equals(Object obj) {
return (this == obj);
}

String

public boolean equals(Object anObject) {
	if (this == anObject) {
		return true;
	} if (anObject instanceof String) {
		String anotherString = (String)anObject;
		int n = value.length;
		if (n == anotherString.value.length) {
			char v1[] = value;
			char v2[] = anotherString.value;
			int i = 0;
			while (n-- != 0) {
				if (v1[i] != v2[i])
				return false;
				i++;
			} return true;
		}
	} return false;
} 

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

public class StringDemo {
	public static void main(String args[]) {
		String str1 = "Hello";
		String str2 = new String("Hello");
		String str3 = str2; // 引用传递
        System.out.println(str1 == str2); // false
        System.out.println(str1 == str3); // false
        System.out.println(str2 == str3); // true
        System.out.println(str1.equals(str2)); // true
        System.out.println(str1.equals(str3)); // true
        System.out.println(str2.equals(str3)); // true
	}
}

8、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的两个
    对象无论如何都不会相等(即使这两个对象指向相同的数据)

9、final

final 类不能被继承,所有成员方法都会被隐式地指定为 final 方法,final 方法不能被重写。

final 变量表示常量,只能被赋值一次,赋值后值不再改变。

修饰基本数据类型时,该值在初始化后不能改变。
修饰引用类型时,引用指向的对象在初始化后不能改变,但该对象的内容可以发生变化。

内存语义

编译器会在 final 域的写后,构造方法的 return 前插入一个 Store Store 屏障,确保对象引用为任意线程可见前其 final 域已初始化。
编译器在读 final 域操作的前面插入一个 Load Load 屏障,确保在读一个对象的 final 域前一定会先读包含这个 final 域的对象引用。

最终的

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

(1)修饰成员变量

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

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

public class FinalVar {
	final static int a = 0;//类变量:再声明的时候就需要赋值 或者静态代码块赋值
    /**
    static{
    a = 0;
    } *
    /
	final int b = 0;//再声明的时候就需要赋值 或者代码块中赋值 或者构造器赋值
	/*{
    b = 0;
    }*/
	public static void main(String[] args) {
		final int localA; //局部变量只声明没有初始化,不会报错,与final无关。
		localA = 0;//在使用之前一定要赋值
		//localA = 1; 但是不允许第二次赋值
	}
}

(3)修饰基本类型数据和引用类型数据

  • 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
  • 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变的
public class FinalReferenceTest{
    public static void main(){
        final int[] iArr={1,2,3,4};
        iArr[2]=-3;//合法
        iArr=null;//非法,对iArr不能重新赋值
        final Person p = new Person(25);
        p.setAge(24);//合法
        p=null;//非法
    }
}

为什么局部内部类和匿名内部类只能访问局部final变量?
编译之后会生成两个class文件,Test.class Test1.class

public class Test {
    public static void main(String[] args) {
    } 
    //局部final变量a,b
	public void test(final int b) {//jdk8在这里做了优化, 不用写,但实际上也是有的,也不能修改
        final int a = 10;
        //匿名内部类
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
	}
} 
class OutClass {
    private int age = 12;
    public void outPrint(final int x) {
            class InClass {
                public void InPrint() {
                    System.out.println(x);
                    System.out.println(age);
                }
        	} new InClass().InPrint();
    }
}

首先需要知道的一点是: 内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着
方法的执行完毕就被销毁。

这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有
没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解
决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以
访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期

将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修
改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?

就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量
和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。

10、String、StringBuffer、StringBuilder

String是final修饰的,不可变,每次操作都会产生新的String对象
StringBuffer和StringBuilder都是在原对象上操作
StringBuffer是线程安全的,StringBuilder线程不安全的
StringBuffer方法都是synchronized修饰的
性能:StringBuilder > StringBuffer > String

场景:经常需要改变字符串内容时使用后面两个
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer

11、重载和重写的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MtxKeHvL-1637023656092)(file://C:\Users\xiaoting\Desktop%E5%BD%93%E5%89%8D%E9%A2%98%E7%9B%AE%E5%92%8C%E7%AD%94%E6%A1%88\images%E9%87%8D%E8%BD%BD%E5%92%8C%E9%87%8D%E5%86%99%E7%9A%84%E5%8C%BA%E5%88%AB.png?lastModify=1636424981)]

重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问
修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于
等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方
法。

public int add(int a,String b)
public String add(int a,String b)
//编译报错

12、接口和抽象类的区别

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

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

而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。

抽象类是对类本质的抽象,表达的是 is a 的关系,比如: BMW is a Car 。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是 like a 的关系。比如: Bird like a Aircraft (像飞行器一样可以飞)但其本质上 is a Bird 。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。

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

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

13、Object 有哪些方法

  • equals | 检测对象是否相等,默认使用 == 比较,可以重写该方法自定义规则。
  • hashCode | 每个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。
  • toString | 默认打印表示对象值的一个字符串。
  • clone | 默认声明为 protected,只能由本类对象调用,且是浅拷贝。一般重写 clone 方法需要实现 Cloneable 接口并声明为 public,如果没有实现 Cloneable 接口会抛出 CloneNotSupport 异常。
  • finalize | GC 判断垃圾时,如果对象没有与 GC Roots 相连会被第一次标记,之后判断对象是否有必要执行 finalize 方法,有必要则由一条低调度优先级的 Finalizer 线程执行。虚拟机会触发该方法但不保证结束,防止方法执行缓慢或发生死循环。只要对象在 finalize 方法中重新与引用链相连,就会在第二次标记时移出回收集合。由于运行代价高且具有不确定性,在 JDK9 标记为过时方法。
  • getClass | 返回对象所属类的 Class 对象。
  • wait | 阻塞持有该对象锁的线程。
  • notify | 唤醒持有该对象锁的线程,notify 随机唤醒一个线程,notifyAll | 唤醒全部线程

14、String有哪些方法

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

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

16、Java类加载器

JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。
AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。系统类加载器,线程上下文加载器
继承ClassLoader实现自定义类加载器

17、双亲委托模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q905DSl2-1637023656101)(C:\Users\xiaoting\AppData\Roaming\Typora\typora-user-images\image-20211101202656414.png)]

双亲委派模型的好处:

  • 主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
  • 同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不
    同的 ClassLoader加载就是不同的两个类

18、泛型和泛型擦除

泛型本质是参数化类型,解决不确定对象具体类型的问题。

泛型的好处:① 类型安全,不存在 ClassCastException。② 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的数据类型。

泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。例如定义 ListList,在编译后都会变成 List 。

19、反射

在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射,缺点是破坏了封装性及泛型约束。

20、jdk1.8的新特性有哪些

一、接口的默认方法

Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下:

代码如下:

interface Formula { double calculate(int a);

default double sqrt(int a) { return Math.sqrt(a); } }

Formula接口在拥有calculate方法之外同时还定义了sqrt方法,实现了Formula接口的子类只需要实现一个calculate方法,默认方法sqrt将在子类上可以直接使用。

代码如下:

Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } };

formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0

文中的formula被实现为一个匿名类的实例,该代码非常容易理解,6行代码实现了计算 sqrt(a * 100)。在下一节中,我们将会看到实现单方法接口的更简单的做法。

译者注: 在Java中只有单继承,如果要让一个类赋予新的特性,通常是使用接口来实现,在C++中支持多继承,允许一个子类同时具有多个父类的接口与功能,在其他语言中,让一个类同时具有其他的可复用代码的方法叫做mixin。新的Java 8 的这个特新在编译器实现的角度上来说更加接近Scala的trait。 在C#中也有名为扩展方法的概念,允许给已存在的类型扩展方法,和Java 8的这个在语义上有差别。

二、Lambda 表达式

首先看看在老版本的Java中是如何排列字符串的:

代码如下:

List names = Arrays.asList(“peterF”, “anna”, “mike”, “xenia”);

Collections.sort(names, new Comparator() { @Override public int compare(String a, String b) { return b.compareTo(a); } });

只需要给静态方法 Collections.sort 传入一个List对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给sort方法。

在Java 8 中你就没必要使用这种传统的匿名对象的方式了,Java 8提供了更简洁的语法,lambda表达式:

代码如下:

Collections.sort(names, (String a, String b) -> { return b.compareTo(a); });

看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短:

代码如下:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

对于函数体只有一行代码的,你可以去掉大括号{}以及return关键字,但是你还可以写得更短点:

代码如下:

Collections.sort(names, (a, b) -> b.compareTo(a));

Java编译器可以自动推导出参数类型,所以你可以不用再写一次类型。接下来我们看看lambda表达式还能作出什么更方便的东西来:

三、函数式接口

Lambda表达式是如何在java的类型系统中表示的呢?每一个lambda表达式都对应一个类型,通常是接口类型。而“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。因为 默认方法 不算抽象方法,所以你也可以给你的函数式接口添加默认方法。

我们可以将lambda表达式当作任意只包含一个抽象方法的接口类型,确保你的接口一定达到这个要求,你只需要给你的接口添加 @FunctionalInterface 注解,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的。

示例如下:

代码如下:

@FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert(“123”); System.out.println(converted); // 123

需要注意如果@FunctionalInterface如果没有指定,上面的代码也是对的。

译者注 将lambda表达式映射到一个单方法的接口上,这种做法在Java 8之前就有别的语言实现,比如Rhino JavaScript解释器,如果一个函数参数接收一个单方法的接口而你传递的是一个function,Rhino 解释器会自动做一个单接口的实例到function的适配器,典型的应用场景有 org.w3c.dom.events.EventTarget 的addEventListener 第二个参数 EventListener。

四、方法与构造函数引用

前一节中的代码还可以通过静态方法引用来表示:

代码如下:

Converter converter = Integer::valueOf; Integer converted = converter.convert(“123”); System.out.println(converted); // 123

Java 8 允许你使用 :: 关键字来传递方法或者构造函数引用,上面的代码展示了如何引用一个静态方法,我们也可以引用一个对象的方法:

代码如下:

converter = something::startsWith; String converted = converter.convert(“Java”); System.out.println(converted); // “J”

接下来看看构造函数是如何使用::关键字来引用的,首先我们定义一个包含多个构造函数的简单类:

代码如下:

class Person { String firstName; String lastName;

Person() {}

Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }

接下来我们指定一个用来创建Person对象的对象工厂接口:

代码如下:

interface PersonFactory

{ P create(String firstName, String lastName); }

这里我们使用构造函数引用来将他们关联起来,而不是实现一个完整的工厂:

代码如下:

PersonFactory personFactory = Person::new; Person person = personFactory.create(“Peter”, “Parker”);

我们只需要使用 Person::new 来获取Person类构造函数的引用,Java编译器会自动根据PersonFactory.create方法的签名来选择合适的构造函数。

五、Lambda 作用域

在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。

六、访问局部变量

我们可以直接在lambda表达式中访问外层的局部变量:

代码如下:

final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num);

stringConverter.convert(2); // 3

但是和匿名对象不同的是,这里的变量num可以不用声明为final,该代码同样正确:

代码如下:

int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num);

stringConverter.convert(2); // 3

不过这里的num必须不可被后面的代码修改(即隐性的具有final的语义),例如下面的就无法编译:

代码如下:

int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;

在lambda表达式中试图修改num同样是不允许的。

七、访问对象字段与静态变量

和本地变量不同的是,lambda内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的:

代码如下:

class Lambda4 { static int outerStaticNum; int outerNum;

void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); };

Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }

八、访问接口的默认方法

还记得第一节中的formula例子么,接口Formula定义了一个默认方法sqrt可以直接被formula的实例包括匿名对象访问到,但是在lambda表达式中这个是不行的。 Lambda表达式中是无法访问到默认方法的,以下代码将无法编译:

代码如下:

Formula formula = (a) -> sqrt( a * 100); Built-in Functional Interfaces

JDK 1.8 API包含了很多内建的函数式接口,在老Java中常用到的比如Comparator或者Runnable接口,这些接口都增加了@FunctionalInterface注解以便能用在lambda上。 Java 8 API同样还提供了很多全新的函数式接口来让工作更加方便,有一些接口是来自Google Guava库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的。

Predicate****接口

Predicate 接口只有一个参数,返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非):

代码如下:

Predicate predicate = (s) -> s.length() > 0;

predicate.test(“foo”); // true predicate.negate().test(“foo”); // false

Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull;

Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();

Function 接口

Function 接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法(compose, andThen):

代码如下:

Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf);

backToString.apply(“123”); // “123”

Supplier 接口 Supplier 接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数

代码如下:

Supplier personSupplier = Person::new; personSupplier.get(); // new Person

Consumer 接口 Consumer 接口表示执行在单个参数上的操作。

代码如下:

Consumer greeter = § -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person(“Luke”, “Skywalker”));

Comparator 接口 Comparator 是老Java中的经典接口, Java 8在此之上添加了多种默认方法:

代码如下:

Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);

Person p1 = new Person(“John”, “Doe”); Person p2 = new Person(“Alice”, “Wonderland”);

comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

Optional 接口

Optional 不是函数是接口,这是个用来防止NullPointerException异常的辅助类型,这是下一届中将要用到的重要概念,现在先简单的看看这个接口能干什么:

Optional 被定义为一个简单的容器,其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null,而在Java 8中,不推荐你返回null而是返回Optional。

代码如下:

Optional optional = Optional.of(“bam”);

optional.isPresent(); // true optional.get(); // “bam” optional.orElse(“fallback”); // “bam”

optional.ifPresent((s) -> System.out.println(s.charAt(0))); // “b”

Stream 接口

java.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 java.util.Collection的子类,List或者Set, Map不支持。Stream的操作可以串行执行或者并行执行。

首先看看Stream是怎么用,首先创建实例代码的用到的数据List:

代码如下:

List stringCollection = new ArrayList<>(); stringCollection.add(“ddd2”); stringCollection.add(“aaa2”); stringCollection.add(“bbb1”); stringCollection.add(“aaa1”); stringCollection.add(“bbb3”); stringCollection.add(“ccc”); stringCollection.add(“bbb2”); stringCollection.add(“ddd1”);

Java 8扩展了集合类,可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建一个Stream。下面几节将详细解释常用的Stream操作:

Filter 过滤

过滤通过一个predicate接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作(比如forEach)。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作,所以我们不能在forEach之后来执行其他Stream操作。

代码如下:

stringCollection .stream() .filter((s) -> s.startsWith(“a”)) .forEach(System.out::println);

// “aaa2”, “aaa1”

Sort 排序

排序是一个中间操作,返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。

代码如下:

stringCollection .stream() .sorted() .filter((s) -> s.startsWith(“a”)) .forEach(System.out::println);

// “aaa1”, “aaa2”

需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据stringCollection是不会被修改的:

代码如下:

System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map 映射 中间操作map会将元素根据指定的Function接口来依次将元素转成另外的对象,下面的示例展示了将字符串转换为大写字符串。你也可以通过map来讲对象转换成其他类型,map返回的Stream类型是根据你map传递进去的函数的返回值决定的。

代码如下:

stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println);

// “DDD2”, “DDD1”, “CCC”, “BBB3”, “BBB2”, “AAA2”, “AAA1”

Match 匹配

Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回一个boolean类型的值。

代码如下:

boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith(“a”));

System.out.println(anyStartsWithA); // true

boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith(“a”));

System.out.println(allStartsWithA); // false

boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith(“z”));

System.out.println(noneStartsWithZ); // true

Count 计数 计数是一个最终操作,返回Stream中元素的个数,返回值类型是long。

代码如下:

long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith(“b”)) .count();

System.out.println(startsWithB); // 3

Reduce 规约

这是一个最终操作,允许通过指定的函数来讲stream中的多个元素规约为一个元素,规越后的结果是通过Optional接口表示的:

代码如下:

Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + “#” + s2);

reduced.ifPresent(System.out::println); // “aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2”

并行****Streams

前面提到过Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。

下面的例子展示了是如何通过并行Stream来提升性能:

首先我们创建一个没有重复元素的大表:

代码如下:

int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }

然后我们计算一下排序这个Stream要耗时多久, 串行排序:

代码如下:

long t0 = System.nanoTime();

long count = values.stream().sorted().count(); System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format(“sequential sort took: %d ms”, millis));

// 串行耗时: 899 ms 并行排序:

代码如下:

long t0 = System.nanoTime();

long count = values.parallelStream().sorted().count(); System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format(“parallel sort took: %d ms”, millis));

// 并行排序耗时: 472 ms 上面两个代码几乎是一样的,但是并行版的快了50%之多,唯一需要做的改动就是将stream()改为parallelStream()。

Map

前面提到过,Map类型不支持stream,不过Map提供了一些新的有用的方法来处理一些日常任务。

代码如下:

Map map = new HashMap<>();

for (int i = 0; i < 10; i++) { map.putIfAbsent(i, “val” + i); }

map.forEach((id, val) -> System.out.println(val)); 以上代码很容易理解, putIfAbsent 不需要我们做额外的存在性检查,而forEach则接收一个Consumer接口来对map里的每一个键值对进行操作。

下面的例子展示了map上的其他有用的函数:

代码如下:

map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33

map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false

map.computeIfAbsent(23, num -> “val” + num); map.containsKey(23); // true

map.computeIfAbsent(3, num -> “bam”); map.get(3); // val33

接下来展示如何在Map里删除一个键值全都匹配的项:

代码如下:

map.remove(3, “val3”); map.get(3); // val33

map.remove(3, “val33”); map.get(3); // null

另外一个有用的方法:

代码如下:

map.getOrDefault(42, “not found”); // not found

对Map的元素做合并也变得很容易了:

代码如下:

map.merge(9, “val9”, (value, newValue) -> value.concat(newValue)); map.get(9); // val9

map.merge(9, “concat”, (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat

Merge做的事情是如果键名不存在则插入,否则则对原键对应的值做合并操作并重新插入到map中。

九、Date API

Java 8 在包java.time下包含了一组全新的时间日期API。新的日期API和开源的Joda-Time库差不多,但又不完全一样,下面的例子展示了这组新API里最重要的一些部分:

Clock 时钟

Clock类提供了访问当前日期和时间的方法,Clock是时区敏感的,可以用来取代 System.currentTimeMillis() 来获取当前的微秒数。某一个特定的时间点也可以使用Instant类来表示,Instant类也可以用来创建老的java.util.Date对象。

代码如下:

Clock clock = Clock.systemDefaultZone(); long millis = clock.millis();

Instant instant = clock.instant(); Date legacyDate = Date.from(instant); // legacy java.util.Date

Timezones 时区

在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of来获取到。 时区定义了到UTS时间的时间差,在Instant时间点对象到本地日期对象之间转换的时候是极其重要的。

代码如下:

System.out.println(ZoneId.getAvailableZoneIds()); // prints all available timezone ids

ZoneId zone1 = ZoneId.of(“Europe/Berlin”); ZoneId zone2 = ZoneId.of(“Brazil/East”); System.out.println(zone1.getRules()); System.out.println(zone2.getRules());

// ZoneRules[currentStandardOffset=+01:00] // ZoneRules[currentStandardOffset=-03:00]

LocalTime 本地时间

LocalTime 定义了一个没有时区信息的时间,例如 晚上10点,或者 17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差:

代码如下:

LocalTime now1 = LocalTime.now(zone1); LocalTime now2 = LocalTime.now(zone2);

System.out.println(now1.isBefore(now2)); // false

long hoursBetween = ChronoUnit.HOURS.between(now1, now2); long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);

System.out.println(hoursBetween); // -3 System.out.println(minutesBetween); // -239

LocalTime 提供了多种工厂方法来简化对象的创建,包括解析时间字符串。

代码如下:

LocalTime late = LocalTime.of(23, 59, 59); System.out.println(late); // 23:59:59

DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedTime(FormatStyle.SHORT) .withLocale(Locale.GERMAN);

LocalTime leetTime = LocalTime.parse(“13:37”, germanFormatter); System.out.println(leetTime); // 13:37

LocalDate 本地日期

LocalDate 表示了一个确切的日期,比如 2014-03-11。该对象值是不可变的,用起来和LocalTime基本一致。下面的例子展示了如何给Date对象加减天/月/年。另外要注意的是这些对象是不可变的,操作返回的总是一个新实例。

代码如下:

LocalDate today = LocalDate.now(); LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); LocalDate yesterday = tomorrow.minusDays(2);

LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4); DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();

System.out.println(dayOfWeek); // FRIDAY 从字符串解析一个LocalDate类型和解析LocalTime一样简单:

代码如下:

DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedDate(FormatStyle.MEDIUM) .withLocale(Locale.GERMAN);

LocalDate xmas = LocalDate.parse(“24.12.2014”, germanFormatter); System.out.println(xmas); // 2014-12-24

LocalDateTime 本地日期时间

LocalDateTime 同时表示了时间和日期,相当于前两节内容合并到一个对象上了。LocalDateTime和LocalTime还有LocalDate一样,都是不可变的。LocalDateTime提供了一些能访问具体字段的方法。

代码如下:

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);

DayOfWeek dayOfWeek = sylvester.getDayOfWeek(); System.out.println(dayOfWeek); // WEDNESDAY

Month month = sylvester.getMonth(); System.out.println(month); // DECEMBER

long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY); System.out.println(minuteOfDay); // 1439

只要附加上时区信息,就可以将其转换为一个时间点Instant对象,Instant时间点对象可以很容易的转换为老式的java.util.Date。

代码如下:

Instant instant = sylvester .atZone(ZoneId.systemDefault()) .toInstant();

Date legacyDate = Date.from(instant); System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014

格式化LocalDateTime和格式化时间和日期一样的,除了使用预定义好的格式外,我们也可以自己定义格式:

代码如下:

DateTimeFormatter formatter = DateTimeFormatter .ofPattern(“MMM dd, yyyy - HH:mm”);

LocalDateTime parsed = LocalDateTime.parse(“Nov 03, 2014 - 07:13”, formatter); String string = formatter.format(parsed); System.out.println(string); // Nov 03, 2014 - 07:13

和java.text.NumberFormat不一样的是新版的DateTimeFormatter是不可变的,所以它是线程安全的。

十、Annotation 注解

在Java 8中支持多重注解了,先看个例子来理解一下是什么意思。 首先定义一个包装类Hints注解用来放置一组具体的Hint注解:

代码如下:

@interface Hints { Hint[] value(); }

@Repeatable(Hints.class) @interface Hint { String value(); }

Java 8允许我们把同一个类型的注解使用多次,只需要给该注解标注一下@Repeatable即可。

例 1: 使用包装类当容器来存多个注解(老方法)

代码如下:

@Hints({@Hint(“hint1”), @Hint(“hint2”)}) class Person {}

例 2:使用多重注解(新方法)

代码如下:

@Hint(“hint1”) @Hint(“hint2”) class Person {}

第二个例子里java编译器会隐性的帮你定义好@Hints注解,了解这一点有助于你用反射来获取这些信息:

代码如下:

Hint hint = Person.class.getAnnotation(Hint.class); System.out.println(hint); // null

Hints hints1 = Person.class.getAnnotation(Hints.class); System.out.println(hints1.value().length); // 2

Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class); System.out.println(hints2.length); // 2

即便我们没有在Person类上定义@Hints注解,我们还是可以通过 getAnnotation(Hints.class) 来获取 @Hints注解,更加方便的方法是使用 getAnnotationsByType 可以直接获取到所有的@Hint注解。 另外Java 8的注解还增加到两种新的target上了:

代码如下:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @interface MyAnnotation {}

关于Java 8的新特性就写到这了,肯定还有更多的特性等待发掘。JDK 1.8里还有很多很有用的东西,比如Arrays.parallelSort, StampedLock和CompletableFuture等等。

二、Java集合

1、List、Set、Map三者的区别

① List 和 Set 实现了 Collection 接口,List 的元素有序可重复、Set 的元素无序不可重复,Map 是以键值对存储元素的。

② List 的实现包括 ArrayList(数组实现)、LinkedList(链表实现)、Vector(线程安全的 ArrayList) 和 Stack(继承 Vector,有栈的语义)。

③ Set 的实现包括 HashSet(通过 HashMap 实现,元素就是 HashMap 的 Key,Value 是一个 Object 类型的常量)、LinkedHashSet(通过 LinkedHashMap 实现)和 TreeSet(可以对元素排序,通过实现 Compare 接口或 Comparator 接口)。

④ Map 的实现主要包括 HashMap、LinkedHashMap(通过 LinkedList 维护插入顺序) 和 TreeMap(可以按 Key 排序,通过实现 Compare 接口或 Comparator 接口)。

2、List和Set的区别

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

3、ArrayList和LinkedList区别

ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象)

LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历
遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大。
另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结果为空时会遍历整个列表。

4、Array List的扩容机制

  1. 构造方法:初始化一个空数组
  2. add方法触发:扩容逻辑、赋值、返回true
    1. 扩容逻辑:第一次直接初始化长度为10的数组;
    2. 随后按照1.5倍扩容,要求传入的长度大于现有的数组长度

6、Hash Map和Hash Set区别

  • Hash Set的底层是基于Hash Map实现的,将Hash Map中的value值统一设置为一个Object对象默认值
  • Hash Set除了clone()、readObject()、writeObject()需要自己编写外,其他方法都可以直接使用Hash Map;

7、Hash Set如何检查重复

当对象要存入Set时,计算对象的哈希值,与set集合中的对象哈希值进行比较,若都不相同,则没有重复;若有相同,则将当前位置的对象与要存入的对象进行哈希值和属性值进行对比,都相同则认为重复;

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

区别 :
(1)HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
(2)HashMap允许key和value为null,而HashTable不允许
(3)HashMap链表插入节点的方式 在Java1.7中,插入链表节点使用头插法。Java1.8中变成了尾插法

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

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

数组扩容

​ 1.8以后不用重新计算hash值,新位置要么是原来的位置,要么是原来位置+原容器的大小

9、hashmap线程安全的方式?

HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全.

方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的. 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现.

方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进.

方法一特点:

通过Collections.synchronizedMap()来封装所有不安全的HashMap的方法,就连toString, hashCode都进行了封装. 封装的关键点有2处,1)使用了经典的synchronized来进行互斥, 2)使用了代理模式new了一个新的类,这个类同样实现了Map接口.在Hashmap上面,synchronized锁住的是对象,所以第一个申请的得到锁,其他线程将进入阻塞,等待唤醒. 优点:代码实现十分简单,一看就懂.缺点:从锁的角度来看,方法一直接使用了锁住方法,基本上是锁住了尽可能大的代码块.性能会比较差.

方法二特点:

重新写了HashMap,比较大的改变有如下几点.使用了新的锁机制,把HashMap进行了拆分,拆分成了多个独立的块,这样在高并发的情况下减少了锁冲突的可能,使用的是NonfairSync. 这个特性调用CAS指令来确保原子性与互斥性.当如果多个线程恰好操作到同一个segment上面,那么只会有一个线程得到运行.

优点:需要互斥的代码段比较少,性能会比较好. ConcurrentHashMap把整个Map切分成了多个块,发生锁碰撞的几率大大降低,性能会比较好. 缺点:代码繁琐

10、Hash Map多线程操作导致死循环

JDK1.7的扩容函数为transfer(),采用头插法进行扩容,在多线程并发操作扩容时,会发生死循环情况

JDK1.8的扩容函数为resize(),采用尾插法解决死循环问题

11、ConcurrentHashMap和hash table的区别

相同:

  • 都是线程安全的

不同:

hash table采用synchronized来保证线程安全,当一个线程访问同步方法时,其他想要访问的线程会进入阻塞或轮询状态,直到当前访问的线程访问结束,才能有机会得到访问机会。

ConcurrentHashMap在JDK1.7时,主要采用Segment对整个桶数组进行分割,默认分成16个,每个锁只会锁一个segment,不同segment不会互相影响,当不同线程访问不同的segment段数据时,不会发生锁竞争,提高了执行效率。JDK1.8开始,采用Node数组+链表/红黑树的数据结构实现线程安全,并使用CAS和synchronized来并发控制。

12、ConcurrentHashMap原理,jdk7和jdk8版本的区别

jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部

锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment,get方法无需加锁,volatile保证
jdk8:
数据结构:synchronized(扩容,hash冲突)+CAS(乐观锁)+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
查找,替换,赋值操作都使用CAS
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见
数组用volatile修饰,保证扩容时被读线程感知

13、红黑树

性质:

  • 根节点为黑色节点;
  • 红色节点的两个子节点必须是黑色节点
  • 所有叶子节点都是黑色节点,且叶节点都为空
  • 任意一个节点到不同叶子节点的路径包含相同数目的黑色节点

操作:

左旋:右儿子变成祖先,右儿子的左儿子变成原来祖先的右儿子

右旋:左儿子变成祖先,左儿子的右儿子变成原来祖先的左儿子

三、线程、并发相关

1、进程与线程

1.1、进程

  • 进程是程序的一次执行,是系统运行的基本单位,程序执行的过程就是进程从创建、到运行、最后到消亡的过程
  • 启动一个main函数,就是启动了一个JVM进程

1.2、线程

  • 线程是系统运行的最小单位,一个进程里面可以有多个线程,线程共享本进程的堆和方法区资源,有自己独立的程序计数器、虚拟栈和本地方法栈。由于上下文切换的开销比进程小,因此被称为轻量级进程。

  • 一个Java程序的运行是main线程和多个其他线程同时运行

1.3、进程与线程的关系

  • 一个进程可以有多个线程,一个线程只能属于一个进程。

  • 一个JVM进程里有堆、方法区、程序计数器、本地方法栈和虚拟栈;JVM进程里的线程可以共享进程的堆和方法区的资源,有自己独立的程序计数器、本地方法栈和虚拟栈。

1.4、进程与线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器调度和执行的基本单位
  • 资源开销:每个进程都有自己独立的代码和数据空间,上下切换开销较大;而线程作为轻量级进程,一个进程里的所有线程共享代码和数据空间,线程开销较小
  • 包含关系:线程是进程的一部分,一个进程里如果包含多个线程,那么执行过程不是一条线,而是多条线共同完成的。
  • 内存分配:同一进程里的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是独立的
  • 影响关系:一个进程崩溃后,在保护模式下,不影响其他进程执行;但一个进程里的一个线程崩溃后,则这个进程将会崩溃;
  • 执行过程:进程是程序运行的入口,顺序执行序列和程序出口。而线程不能单独执行,必须依存在应用程序中。

1.5、程序计数器为什么私有

由它的作用决定的。

  • 指示程序运行的标志:字节解释器通过程序计数器一次读取程序命令,实现代码的流程控制;
  • 多线程任务下,保存当前线程的状态:当一个线程执行完CPU分配的时间片而切换到另外线程时,程序计数器就会记录当前线程运行到了哪一行,等下次线程运行时,直接从这行开始执行程序。

所以,程序计数器私有,否则无法保存当前线程状态

1.6、虚拟栈和本地方法栈为什么私有

由它的作用决定的,以虚拟栈为例

  • 每个Java程序执行时,都会创建一个栈帧,里面包括局部变量表、操作数栈、常量引用池。方法的调用到完成,对应着栈帧在虚拟栈里入栈和出栈的过程。

  • 存数据的时候,所有值压入操作数栈,然后出栈保存到局部变量表。

  • 读取数据,先从局部变量表将值入栈到操作数栈,出栈后CPU进行计算,然后把结果保存到操作数栈,最后再出栈把结果放入局部变量表中

本地方法栈一样,区别是虚拟栈是为虚拟机的方法进行操作,而本地方法栈是为本地方法服务

1.7、堆和方法区

**堆:**是最大的一块内存,存放新创建的对象

**方法区:**存放加载的类信息、静态变量、常量

2、为什么要用多线程

  • 从计算机底层来说:线程是轻量级进程,是程序执行的最小单位,线程上下文切换的开销成本比进程小。在多核CPU时代,多个线程可以同时运行,大大减少线程上下文切换的开销
  • 时代。在单核CPU时代,多线程可以提高CPU和I/O设备的综合利用率。一个线程在需要CPU计算的时候,可以允许让另外线程进行I/O设备操作;在多核CPU时代,多线程可以提高CPU利用率。计算一个复杂任务时,多线程可以同时利用CPU的多个核心进行计算,从而缩短解决时间
  • 需求。如今大多数都是高并发的系统开发,利用好多线程可以大大提高系统整体的并发性能

3、多线程带来的问题

  • 内存泄漏:不再被使用的对象或者变量一直被占据在内存中
  • 上下文切换:一个线程从保存到运行的过程。
  • 死锁:多个线程因长时间请求获取某一资源不成功而一直处于阻塞状态

4、上下文切换

当前任务执行完CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换回这个任务,可以再加载这个任务的状态。任务从保存到再加载的过程就是一个上下文切换

5、如何预防死锁?

  1. 死锁发生的四个必要条件:
    1. 互斥条件 同一时间只能有一个线程获取资源。
    2. 不可剥夺条件 一个线程已经占有的资源,在释放之前不会被其它线程抢占
    3. 请求和保持条件 线程等待过程中不会释放已占有的资源
    4. 循环等待条件 多个线程互相等待对方释放资源
  2. 死锁预防,那么就是需要破坏这四个必要条件
    1. 由于资源互斥是资源使用的固有特性,无法改变,我们不讨论
    2. 破坏不可剥夺条件
      1. 一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行
    3. 破坏请求与保持条件
      1. 第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源
      2. 第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源
    4. 破坏循环等待条件
      1. 采用资源有序分配,将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

6、创建线程有哪些方式?

1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池例如用Executor框架

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vGOGsTf1-1637023656123)(file://C:\Users\xiaoting\Desktop%E5%BD%93%E5%89%8D%E9%A2%98%E7%9B%AE%E5%92%8C%E7%AD%94%E6%A1%88\images\life.jpg?lastModify=1636424981)]

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

1.创建
当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
2.就绪
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。
3.运行
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。添加到了线程列表中,如果OS调度选中了,就会进入到运行状态
4.阻塞
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:

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

  • 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
  • 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。
  • 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
  • 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
  • 线程I/O:线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
  • 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的,并在对实现做出决定时发生。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

5.死亡
线程会以以下三种方式之一结束,结束后就处于死亡状态:

  • run()方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

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

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

sleep:让线程睡眠,期间会出让cpu,在同步代码块中,不会释放锁
wait(必须先获得对应的锁才能调用):让线程进入等待状态,释放当前线程持有的锁资源线程只有在notify 或者notifyAll方法调用后才会被唤醒,然后去争夺锁.
join:线程之间协同方式,使用场景: 线程A必须等待线程B运行完毕后才可以执行,那么就可以在线程A的代码中加入ThreadB.join();
yield:让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

具体:

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 后可能还是有机会重新竞争到锁继续执行的。

yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程

9、sleep()和wait()方法的区别和共同点

  • 锁:sleep()不会释放锁,wait()会释放锁
  • 用途:sleep()用于线程暂停或中断,而wait()用于线程之间的交互和通信
  • 唤醒:sleep()会自动唤醒,wait()需要等notify()或notifyAll()被动唤醒
  • 设置苏醒时间:sleep()必须设置苏醒时间,而wait()不用

10、为什么调用start()方法会执行run()方法,为什么不能直接调用run()方法

  • 当new一个新的线程后,通过调用start()方法使线程进入就绪状态,等到CPU分配相应的时间片时,进入运行状态,然后自动执行run()方法,这是线程正常工作的过程。

  • 直接调用run()方法,只会当作是main线程中的普通方法,无法实现其他线程执行

11、Java 线程通信的方式

Java 采用共享内存模型,线程间的通信总是隐式进行,整个通信过程对程序员完全透明。

volatile 告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可见性。

synchronized 确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子性、可见性和有序性。

等待通知机制指一个线程 A 调用了对象的 wait 方法进入等待状态,另一线程 B 调用了对象的 notify/notifyAll 方法,线程 A 收到通知后结束阻塞并执行后序操作。对象上的 wait 和 notify/notifyAll 完成等待方和通知方的交互。

如果一个线程执行了某个线程的 join 方法,这个线程就会阻塞等待执行了 join 方法的线程终止,这里涉及等待/通知机制。join 底层通过 wait 实现,线程终止时会调用自身的 notifyAll 方法,通知所有等待在该线程对象上的线程。

管道 IO 流用于线程间数据传输,媒介为内存。PipedOutputStream 和 PipedWriter 是输出流,相当于生产者,PipedInputStream 和 PipedReader 是输入流,相当于消费者。管道流使用一个默认大小为 1KB 的循环缓冲数组。输入流从缓冲数组读数据,输出流往缓冲数组中写数据。当数组已满时,输出流所在线程阻塞;当数组首次为空时,输入流所在线程阻塞。

ThreadLocal 是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响。

12、对线程安全的理解

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

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

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

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

栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。

10、synchronized关键字

10.1、锁的原理

在Java中每个对象都拥有一把锁,锁放在对象头中,表示被哪个线程占用

10.2、对象、对象头

  • 对象有对象头、实例数据和填充字节组成
  • 对象头由Mark word与Class Point(指向当前对象类型所在方法区中的类型数据)

10.3、synchronized

  • synchronized可以保证修饰的方法或者代码块在任意时刻只能有一个线程执行
  • 之前版本中,监视锁依赖于底层操作系统的MutexLock来实现的。Java线程是映射到操作系统的原生线程之上的。挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统的线程切换需要从用户态切换到内核态,这个状态的转换需要相对较长的时间,时间成本较高。
  • 锁只能升级,不能降级

10.4、无锁

通过CAS(compare and swap)实现,当线程申请资源时,会一直失败重试,直到操作成功

10.5、偏向锁

  • 避免CAS或线程切换的资源耗费

10.6、轻量级锁

  • 线程和对象的锁定

    • 线程会在自己的虚拟机栈中开辟一块Lock Record的空间
    • Lock Record存着:Mark word副本;指向对象的指针
    • Mark word前30个字符会生成一个指针,指向Lock Record。
  • 自旋:线程自己在不断循环看对象目标是否被释放,如果释放了那么就进行执行以上三步,如果没有释放就进行下一轮循环

  • 适应性自旋:锁的自旋时间有两个条件决定:上一次在同一个锁的自旋时间;锁的状态

  • 如果等待自旋的线程超过一个,就会升级到重量级锁

10.7、重量级锁

需要操作系统在用户态和内核态进行切换,监视器monitorenter和monitorexit都是依赖操作系统的mutexlock实现,很耗时

10.8、synchronized关键字使用

  1. **修饰实例方法:**作用于当前对象实例加锁,进入同步代码块前要获得当前对象实例的锁

  2. **修饰静态方法:**会给当前类加锁,作用于类的所有对象实例,进入同步代码前获得当前类锁。当线程A调用一个实例对象的非静态synchronized方法,线程B仍然可以调用该实例对象所属于类的静态synchronized方法。

  3. **修饰代码块:**指定加锁对象,对给定对象/类加锁。进入同步代码块前要获得给定对象的锁

10.9、synchronized关键字底层原理

10.9.1、synchronized同步语句块的情况
  • 会用monitor对象监视器,当进入的时候会判断锁计数器是否为0,为0就会执行monitorenter指令,计数器加1,否则一直等待其他线程释放锁。执行完同步代码块后,执行monitorexit指令,锁计数器减1.
  • Monitor是基于c++实现的,由ObjectMonitor实现的
  • monitorenter指令是执行同步代码块开始的位置
  • monitorexit指令是执行同步代码块结束的位置
10.9.2、synchronized修饰方法

Synchronized锁方法的时候,会加上ACC_SYNCHRONIZED标识,让JVM识别出该方法是同步方法,直接作为同步方法处理。

10.10、为什么需要CPU高速缓存

**内存缓存:**缓存的是硬盘数据,用于解决硬盘访问速度过慢的问题

**CPU缓存:**缓存的是内存数据,用于解决CPU处理速度与内存不匹配的问题

**缓存一致性协议:**当CPU写数据时,发现变量为共享变量,即其他CPU也有该变量的副本,此时会通知其他CPU将该变量副本的缓存设置为无效状态,当其他CPU再读取该变量是,发现无效,会重新到内存读取这个变量

10.11、JMM(Java内存模型)

  • JDK1.2之前,Java的内存模型是直接从主存里面取变量
  • 现在的内存模型是,线程将变量保存到本地内存(高速缓存),而不是直接从主存读写
  • 可能造成数据不一致:一个线程修改了一个变量值,而另一个线程还在用自己本地内存的变量,造成数据不一致。
  • 解决方法:用volatile修饰变量,保证变量的可见性

10.12、为什么有了MESI(缓存一致)协议,还需要volatile关键词保证可见性

MESI协议:

  • M(modified):代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该CPU中。这个状态的缓存行中的数据和内存中的不一样。
  • E(exclusive):代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。
  • S(shared):代表数据不止存在本地CPU缓存中,还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的。
  • I(invalid):代表该缓存行中的内容时无效的。

原因:

  • 多核情况下,MESI协议是弱一致性协议,不能保证一个线程修改变量后,其他线程立马可见。但是volatile可以保证可见性
  • 正确情况下,只有变量被volatile修饰了,变量所在的缓存行才被赋予一致性的校验功能

10.13、synchronized和volatile关键字的区别

  • volatile是线程同步的轻量级实现,所以性能要比synchronized好。
  • volatile只能修饰变量,而synchronized可以修饰变量、方法和代码块
  • volatile主要解决多线程之间的可见性问题,而synchronized主要解决的是多线程之间访问资源的同步性

11、synchronized和lock有哪些区别?

区别类型 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是JVM的一个接口
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
锁的释放 1、以获取锁的线程执行完同步代码,释放锁2、线程执行发生异常,jvm会让线程释放 在finally中必须释放锁,不然容易造成线程死锁
锁类型 锁可重入、不可中断、非公平 可重入、可判断 可公平(两者皆可)
性能 少量同步 适用于大量同步
支持锁的场景 1. 独占锁 1. 公平锁与非公平锁

12、Thread、Runable的区别

答案有问题:

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

13、对守护线程的理解

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

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

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

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

14、ThreadLocal的原理和使用场景

每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所有ThreadLocal对象及其对应的值
ThreadLocalMap 由一个个 Entry 对象构成
Entry 继承自 WeakReference> ,一个 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来实现这种隔离

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HlyqpZMe-1637023656126)(C:\Users\xiaoting\AppData\Roaming\Typora\typora-user-images\image-20211101205638366.png)]

15、ThreadLocal内存泄露原因,如何避免

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

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

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

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

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值,进而清除掉 。

16、并发、并行、串行的区别

串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行

17、并发的三大特性

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

private long count = 0;
public void calc() {
	count++;
}

1:将 count 从主存读到工作内存中的副本中
2:+1的运算
3:将结果写入工作内存
4:将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)
那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。

关键字:synchronized

可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定
还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

//线程1
boolean stop = false;
while(!stop){
	doSomething();
} /
/线程2
stop = true;

如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还
没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因
此还会一直循环下去。
关键字:volatile、synchronized、final
有序性
虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。

int a = 0;
bool flag = false;
public void write() {
    a = 2; //1
    flag = true; //2
} 
public void multiply() {
    if (flag) { //3
    	int ret = a * a;//4
    }
}

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

关键字:volatile、synchronized
volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则明确的。
synchronized关键字同时满足以上三种特性(原子性,可见性,有序性),但是volatile关键字不满足原子性。
在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或
java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。
我们判断使用volatile还是加锁的唯一依据就是volatile的语义能否满足使用的场景(原子性)

10.volatile

了解 Volatile 吗

保证变量对所有线程可见:当一条线程修改了变量值,新值对于其他线程来说立即可见。
禁止指令重排序优化:使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,lock 引发两件事:① 将当前处理器缓存行的数据写回系统内存。② 使其他处理器的缓存无效。相当于对缓存变量做了一次 store 和 write 操作,让 volatile 变量的修改对其他处理器立即可见。
写 volatile 变量时,把该线程工作内存中的值刷新到主内存;读 volatile 变量时,把该线程工作内存值置为无效,从主内存读取。

  1. 保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
//线程1
boolean stop = false;
while(!stop){
	doSomething();
} /
/线程2
stop = true;

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

  1. 禁止指令重排序优化。
int a = 0;
bool flag = false;
public void write() {
    a = 2; //1
    flag = true; //2
} 
public void multiply() {
    if (flag) { //3
    	int ret = a * a;//4
    }
}

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

第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主
存读取。
inc++; 其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以volatile不能保证线程安全。

12、线程池

12.1、线程池的好处

  1. 降低资源的消耗:通过反复利用已经创建好的线程可以减少线程频繁的创建和销毁的资源消耗
  2. 提高响应速度:当任务到达时,不需要等待线程的创建而直接执行
  3. 提高线程的统一管理:线程时稀缺资源,线程被无限创建,不仅会消耗系统资源,还会降低系统的性能,使用线程池可以进行统一的管理、调优和监控

12.2、实现Runnable接口和Callable接口的区别

  • 在ExecutorService中的submit方法中,输入参数有callable和runnable,runnable接口不会返回任何结果或抛出异常,而callable接口可以。
  • TreadPoolExecutor类->AbstractExecutorService->ExecutorService->Executor
  • 在TreadPoolExecutor类中,执行线程池有两种方法:execute()、submit()

12.3、执行execute()方法和submit()方法的区别是什么

  1. execute()方法用于提交不需要返回结果的任务,所以无法判断任务是否被线程池执行成功与否
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以使用Future对象里的get方法获取返回值

12.4、如何创建线程池

  • 通过Executors和ThreadPoolExecutor来创建

  • Executors的三个静态方法,直接使用Executors.方法名:

    • SingleTreadExecutor:返回一个只有一个线程的线程池
    • FixedTreadPool:返回一个固定线程数量的线程池
    • CachedTreadPool:返回一个可根据实际情况调整线程数量的线程池
  • ExecutorService pool = new ThreadPoolExecutor();

12.5、ThreadPoolExecutor类分析

七大参数:

  • 核心线程数:一般情况下工作的线程数
  • 最大线程数:最大可以开的线程数
  • 阻塞队列:最大线程数满了,之后来的线程会进入阻塞队列
  • 存活时间:核心线程空闲一定时间后关闭线程池
  • 存活时间单位:
  • 线程工厂:创建线程的工厂
  • 拒绝策略:阻塞队列也满了,之后的线程处理方式

四大拒绝策略:

  • 中断并抛出异常
  • 丢弃到达的任务(默认)
  • 丢弃阻塞队列中等待时间最长的任务
  • 让提交任务的线程去执行任务

12.6、线程池原理

提交任务后,

  • 判断核心线程池是否已满:
    • 否,创建线程执行任务
    • 是,执行下一步
  • 判断阻塞队列是否已满:
    • 否,将任务放入队列
    • 是,执行下一步
  • 判断线程池是否已满:
    • 否,继续创建线程执行任务
    • 是,执行下一步
  • 执行拒绝策略

13、为什么用线程池?线程池参数?

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

  • corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程

  • maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数

  • keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime 来设置空闲时间

  • workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程

  • ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂

  • Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝

14、简述线程池处理流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbb57CT7-1637023656129)(C:\Users\xiaoting\AppData\Roaming\Typora\typora-user-images\image-20211101213118271.png)]

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

1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源
2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数
(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积
压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超
过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还
是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。

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

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

四、Java异常处理方式

1.Java中的异常体系

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

Java 通过面向对象的方法进行异常处理,一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是一个对

象,它是 Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、 catch、throw、throws 和 finally。

在Java应用中,异常的处理机制分为声明异常,抛出异常和捕获异常。

throw和throws的区别:
(1)位置不同:
throw:方法内部
throws: 方法的签名处,方法的声明处

(2)内容不同:
throw+异常对象(检查异常,运行时异常)
throws+异常的类型(可以多个类型,用,拼接)

(3)作用不同:
throw:异常出现的源头,制造异常。
throws:在方法的声明处,告诉方法的调用者,这个方法中可能会出现我声明的这些异常。然后调用者对这个异常进行处理:
要么自己处理要么再继续向外抛出异常

2.throws声明异常

通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下

去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。注意

非检查异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。

​ 一个方法出现编译时异常,就需要 try-catch/ throws 处理,否则会导致编译错误

3.throw抛出异常

如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。 throw关键字作用是在方法内部抛出一个Throwable类型的异常。任何Java代码都可以通过throw语句抛出异常。

4.trycatch捕获异常

程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。如何选择异常类型

可以根据下图来选择是捕获异常,声明异常还是抛出异常

自定义异常在生产中如何应用

Java虽然提供了丰富的异常处理类,但是在项目中还会经常使用自定义异常,其主要原因是Java提供的异常类在某些情况下还是不能满足实际需球。例如以下情况:
1、系统中有些错误是符合Java语法,但不符合业务逻辑。

2、在分层的软件结构中,通常是在表现层统一对系统其他层次的异常进行捕获处理。

松耦合:

单一职责原则、接口分离原则、依赖倒置原则DIP

五、spring

1、spring是什么?

​ 轻量级的开源的J2EE框架。它是一个容器框架,用来装javabean(java对象),中间层框架(万能胶)可以起一个连接作用,比如说把Struts和hibernate粘合在一起运用,可以让我们的企业开发更快、更简洁
​ Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架
–从大小与开销两方面而言Spring都是轻量级的。
​ --通过控制反转(IoC)的技术达到松耦合的目的
​ --提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发
–包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器。
​ --将简单的组件配置、组合成为复杂的应用,这个意义上是一个框架。

2、如何实现一个IOC容器

​ 1、配置文件配置包扫描路径
​ 2、递归包扫描获取.class文件
​ 3、反射、确定需要交给IOC管理的类
​ 4、对需要注入的类进行依赖注入

  • 配置文件中指定需要扫描的包路径
  • 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解
  • 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以.class结尾的文件添加到一个Set集合中进行存储遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象(Beandefinition)
  • 遍历这个IOC容器,获取到每一个类的实例,判断里面是有有依赖其他的类的实例,然后进行递归注入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KvEchvLg-1637023656131)(总结最终版.assets/image-20211111164804721.png)]

public class IocDemo{
	public static void main (String[] args){
        //1.耦合度高
        UserServiceImpl userservice = new UserServiceImpl();
        //2.简单工厂,对象太多,需要大量标识,就需要更改大量代码
        BaseService userservice = Factory.getBean("user");
        //3.反射
        BaseService userservice = Factory.getBean("com.ting.service.impl.RoleServiceImpl");
        //注:
        //xml中配置bean  
        //new ClassPathXmlApplicationContext("xml");
    }    
}
//简单工厂 设计模式 通过一个方法 传入一个标识 生产对应的对象
class Factory{
    public static BaseService getBean(String beanName){
        BaseService baseService = null;
        if("user".equals(beanName)){
            baseService = new UserServiceImpl();
        }
        if("role".equals(beanName)){
            baseService = new RoleServiceImpl();
        }
        return baseService;
    }
    //通过反射创建对象
    public static BaseService getBean(String className){
        BaseService baseService = null;
        try{
            baseService = (BaseService) Class.forName(className).newInstance();
        }catch(Exception e){
            e.printStackTrace();
        }
        return baseService;
    }
}

3、说一下使用spring的优势?

​ 1、Spring通过DI、AOP和消除样板式代码来简化企业级Java开发

​ 2、Spring框架之外还存在一个构建在核心框架之上的庞大生态圈,它将Spring扩展到不同的领域,如Web服务、REST、移动开发以及NoSQL

​ 3、低侵入式设计,代码的污染极低

​ 4、独立于各种应用服务器,基于Spring框架的应用,可以真正实现Write Once,Run Anywhere的承诺

​ 5、Spring的IoC容器降低了业务对象替换的复杂性,提高了组件之间的解耦

​ 6、Spring的AOP支持允许将一些通用任务如安全、事务、日志等进行集中式处理,从而提供了更好的复用

​ 7、Spring的ORM和DAO提供了与第三方持久层框架的的良好整合,并简化了底层的数据库访问

​ 8、Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可自由选用Spring框架的部分或全部

4、Spring是如何简化开发的?

​ 基于POJO的轻量级和最小侵入性编程

​ 通过依赖注入和面向接口实现松耦合

​ 基于切面和惯例进行声明式编程

​ 通过切面和模板减少样板式代码

5、谈谈你对AOP的理解

​ 系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。
在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

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

​ 在不修改代码的情况下可以对业务代码进行增强,减少重复

6、谈谈你对IOC的理解

UserService service = new UserService; //耦合度高,维护不方便

引入IOC,将创建对象的控制权交给spring的IOC,以前由程序员自己控制对象创建,现在交给IOC容器去创建,如果要使用对象,需要通过DI(依赖注入)@Autowired自动注入 就可以使用对象;

优点:集中管理对象、方便维护,降低耦合度

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

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

控制反转:
没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
引入IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。

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

7、列举一些重要的Spring模块

  • Spring Core:可以说Spring其他所有的功能都需要依赖该类库。主要提供IoC依赖注入功能
  • Spring AOP: 提供面向切面的编程实现。
  • Spring JDBC: Java数据库连接。
  • Spring ORM: 用于支持Hibernate等ORM(对象关系映射)工具。
  • Spring Web:为创建Web应用程序提供支持。
  • Spring Test:提供了对Junit测试的支持。

8、@RestController和@Controller

  • Controller 返回一个页面

单独使用@Controller不加@ResponseBody的话一般都会返回一个视图,这个情况属于比较传统的Spring MVC的应用,对应于前后端不分离的情况。

  • RestController返回JSON或XML形式数据

@RestController只返回对象,对象数据直接以JSON或XML形式写入HTTP响应(Response)中,这种情况属于RESTFUL WEB服务,对应于(前后端分离)

  • Controller+ResponseBody返回JSON或XML形式数据

如果想在Spring4之前开发RESTFUL Web服务的话,只需要结合ResponseBody即可。

9、@Component和@Bean的区别是什么?

  • 作用对象不同:@Component注解作用于类,而@Bean注解作用于方法,因为这个方法返回的就是new 类。
  • @Component通常是通过类路径扫描来自动装配到Spring容器中(会调用ComponentScan注解去扫描路径)。@Bean告诉了Spring这是某个类的示例,当我们需要用它的时候就给我。
  • 当我们要引用第三方库的类时,我们只能用@Bean。

10、将一个类声明为Spring的bean的注解有哪些

  • 我们一般使用@Autowired注解自动装配bean,要想把类标识成@Autowired能自动装配的bean,采用以下注解实现:
  • @Component:通用的注解,可标注任意类为Spring组件。如果一个Bean不知道属于哪个层,可以使用@Component注解标注。
  • @Repository:对应持久层即Dao层,主要有用于数据库的操作。
  • @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到Dao层。
  • @Controller:对应Spring MVC控制层,主要用于接受用户请求并调用Service层返回数据给前端页面。

11、Spring配置Bean的方式:

​ 1.xml:

​ 2.注解:@Autowired @Component(@Controller,@Service,@Respostory);前提:配置扫描包

​ 底层通过反射调用构造方法

​ 3.javaConfig:@Bean

​ 结合@Configuration一起使用,@Bean标注在方法上面,返回一个bean,可以控制实例化过程

​ 4.@Import 3种

注:

两种:一般不用ApplicationContext()

1.xml

​ Spring容器:ClassPathXmlApplicationContext(“xxx.xml”) 传配置文件

​ Spring.xml

​ 扫描包:

2.javaconfig

​ Spring容器:AnnotationConfigApplication(“config.class”) 传配置类

​ 配置类:@Configuration

​ @Bean @Scope @Lazy

​ @ComponentScan

​ @Impoer

11、Bean的注入方式/实例化bean:

  • 构造器注入(反射):1.参数名称 2.参数下标 3.参数类型
  • 静态工厂注入:编写一个静态的工厂方法,这个工厂方法会返回一个我们需要的值,然后在配置文件中,我们指定使用这个工厂方法创建bean。注意是静态方法! factory-method
  • 实例工厂注入(@Bean):实例工厂,需要一个实例对象,才能调用它的工厂方法。factory-bean + factory-method
  • FactoryBean方式

12、通过反射获得Class

  • Class.forName(类的路径);
  • 类名.class;
  • 对象.getClass()

13、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较多时,程序启动较慢。

  • BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使ContextLoader。

  • BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

14、描述一下Spring Bean的生命周期?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tzy0oq8r-1637023656133)(file://C:\Users\xiaoting\Desktop%E5%BD%93%E5%89%8D%E9%A2%98%E7%9B%AE%E5%92%8C%E7%AD%94%E6%A1%88\images\bean%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png?lastModify=1636424981)]

​ 1、解析类得到BeanDefinition
​ 2、如果有多个构造方法,则要推断构造方法
​ 3、确定好构造方法后,进行实例化得到一个对象
​ 4、对对象中的加了@Autowired注解的属性进行属性填充
​ 5、回调Aware方法,比如BeanNameAware,BeanFactoryAware
​ 6、调用BeanPostProcessor的初始化前的方法
​ 7、调用初始化方法
​ 8、调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
​ 9、如果当前创建的bean是单例的则会把bean放入单例池
​ 10、使用bean
​ 11、Spring容器关闭时调用DisposableBean中destory()方法

1、实例化bean对象

​ 通过反射的方式进行对象的创建,此时的创建只是在堆空间中申请空间,属性都是默认值

2、设置对象属性

​ 给对象中的属性进行值的设置工作

3、检查Aware相关接口并设置相关依赖

​ 如果对象中需要引用容器内部的对象,那么需要调用aware接口的子类方法来进行统一的设置

4、BeanPostProcessor的前置处理

​ 对生成的bean对象进行前置的处理工作

5、检查是否是InitializingBean的子类来决定是否调用afterPropertiesSet方法

​ 判断当前bean对象是否设置了InitializingBean接口,然后进行属性的设置等基本工作

6、检查是否配置有自定义的init-method方法

​ 如果当前bean对象定义了初始化方法,那么在此处调用初始化方法

7、BeanPostProcessor后置处理

​ 对生成的bean对象进行后置的处理工作

8、注册必要的Destruction相关回调接口

​ 为了方便对象的销毁,在此处调用注销的回调接口,方便对象进行销毁操作

9、获取并使用bean对象

​ 通过容器来获取对象并进行使用

10、是否实现DisposableBean接口

​ 判断是否实现了DisposableBean接口,并调用具体的方法来进行对象的销毁工作

11、是否配置有自定义的destory方法

​ 如果当前bean对象定义了销毁方法,那么在此处调用销毁方法

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

  • singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。

  • prototype:为每一个bean请求提供一个实例。在每次注入时都会创建一个新的对象

  • request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。

  • session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。

  • application:bean被定义为在ServletContext的生命周期中复用一个单例对象。

  • websocket:bean被定义为在websocket的生命周期中复用一个单例对象。

  • global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。

16、Spring框架中的单例Bean是线程安全的么?

如果在类中声明成员变量,并且有读写操作,就会线程不安全;

解决—》1.设置为多例,2.将成员变量放在Threadlocal,3.同步锁synchronized

但是只需要把成员变量声明在方法中,单例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等这些实现线程同步的方法了。

17、Spring 框架中都用到了哪些设计模式?

简单工厂:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。

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

工厂方法:

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

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点

spring对单例的实现: spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。

适配器模式:

Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法。这样在扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展了。

装饰器模式:动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。

Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。

动态代理:

切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。
织入:把切面应用到目标对象并创建新的代理对象的过程

观察者模式:

spring的事件驱动模型使用的是 观察者模式 ,Spring中Observer模式常用的地方是listener的实现

策略模式:

Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了Resource 接口来访问底层资源。

模板方法:父类定义了骨架(调用哪些方法及顺序),某些特定方法由子类实现。
最大的好处:代码复用,减少重复代码。除了子类要实现的特定方法,其他方法及方法调用顺序都在父类中预先写好了。

refresh方法

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

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

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

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

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

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

spring事务隔离级别就是数据库的隔离级别:外加一个默认级别
read uncommitted(未提交读)
read committed(提交读、不可重复读)
repeatable read(可重复读)
serializable(可串行化)

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

19、spring事务传播机制

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

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

REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
NEVER:不使用事务,如果当前事务存在,则抛出异常
NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)

和REQUIRES_NEW的区别
REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。 在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。
和REQUIRED的区别
REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚 而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响

20、Spring事务传播行为的实现

1.配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。

2.spring在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。

3.真正的数据库的事务和回滚是通过binlog或者redo log实现的。

21、spring事务什么时候会失效?

spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有
如下几种
1、发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而
是UserService对象本身!
解决方法很简单,让那个this变成UserService的代理类即可!
2、方法不是public的

@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。

3、数据库不支持事务
4、没有被spring管理
5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

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

​ bean的自动装配指的是bean的属性值在进行注入的时候通过某种特定的规则和方式去容器中查找,并设置到具体的对象属性中,主要有五种方式:

​ no – 缺省情况下,自动配置是通过“ref”属性手动设定,在项目中最常用
​ byName – 根据属性名称自动装配。如果一个bean的名称和其他bean属性的名称是一样的,将会自装配它。
​ byType – 按数据类型自动装配,如果bean的数据类型是用其它bean属性的数据类型,兼容并自动装配它。
​ constructor – 在构造函数参数的byType方式。
​ autodetect – 如果找到默认的构造函数,使用“自动装配用构造”; 否则,使用“按类型自动装配”。

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


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

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

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

    Cutomer的属性名称是person,Spring会将bean id为person的bean通过setter方法进行自动装配。
    
    
    
  • byType-根据bean的类型进行自动装配。

    Cutomer的属性person的类型为Person,Spirng会将Person类型通过setter方法进行自动装配。
    
    
    
  • constructor-类似byType,不过是应用于构造器的参数。如果一个bean与构造器参数的类型形同,则进行自动装配,否则导致异常。

    Cutomer构造函数的参数person的类型为Person,Spirng会将Person类型通过构造方法进行自动装配。
    
    
    
  • autodetect-如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配。

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

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

六、SpringMVC SpringBoot

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

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

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

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

2、MVC简介

  • Model1(视图层+模型层):

整个web项目都是用JSP写的,只有少量的JavaBean去和数据库打交道。缺点:①前端后端依赖严重,难以进行测试并开发效率低。②控制逻辑和表现逻辑是混杂一起,导致代码复用率低。

  • Model2:

整个是由Javabean(Model)+JSP(View)+Servlet(Controller)。这种开发模式就是早期的JavaWeb MVC,但是用这种模式开发不可避免地会重复造轮子,这会大大降低程序的可维护性和复用性。

  • Spring MVC

把后端项目分成Service层(处理业务)、Dao层(数据库操作)、Entity层(实体类)、Controller(控制层,返回数据给前台页面)。

Spring MVC是Spring Framework的一部分,是基于Java实现MVC的轻量级Web框架

3、SpringMVC 工作流程

当发起请求时被前置的控制器拦截到请求,根据请求参数生成代理请求,找到请求对应的实际控制器,控制器处理请求,创建数据模型,访问数据库,将模型响应给中心控制器,控制器使用模型与视图渲染视图结果,将结果返回给中心控制器,再将结果返回给请求者

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QvziOmyV-1637023656135)(file://C:\Users\xiaoting\Desktop%E5%BD%93%E5%89%8D%E9%A2%98%E7%9B%AE%E5%92%8C%E7%AD%94%E6%A1%88\images\springmvc%E8%BF%90%E8%A1%8C%E6%B5%81%E7%A8%8B.jpg?lastModify=1636424981)]

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

4、MVC流程,@RequestMapping的注解具体怎么实现的?

  1. Spring扫描所有的Bean

2.遍历这些bean,依次判断是否是处理器,并检测其HandlerMethod

3.遍历Handler中的所有方法,找出其中被@RequestMapping注解标记的方法。

4.获取方法method上的@RequestMapping实例

5.检查方法所属的类有没有@RequestMapping注解

6.将类和方法的RequestMapping结合

7.当请求到达时,去UrlMap中找匹配的Url,以及获取对应mapping实例,然后去handerMethods中获取匹配HandlerMethod实例。

8.将RequestMappingInfo实例以及处理方法注册到缓存中。

5、Spring MVC的主要组件?

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

1、HandlerMapping
initHandlerMappings(context),处理器映射器,根据用户请求的资源url来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事。

2、HandlerAdapter
initHandlerAdapters(context),适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。

3、HandlerExceptionResolver
initHandlerExceptionResolvers(context), 其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在SpringMVC中就HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。

4、ViewResolver
initViewResolvers(context),ViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。

5、RequestToViewNameTranslator
initRequestToViewNameTranslator(context),ViewResolver是根据ViewName查找View,但有的Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。RequestToViewNameTranslator在Spring MVC容器里只可以配置一个,所以所有request到ViewName的转换规则都要在一个Translator里面全部实现。

6、LocaleResolver
initLocaleResolver(context), 解析视图需要两个参数:一是视图名,另一个是Locale。视图名是处理器返回的,Locale是从哪里来的?这就是LocaleResolver要做的事情。LocaleResolver用于从request解析出Locale,Locale就是zh-cn之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。

7、ThemeResolver
initThemeResolver(context),用于解析主题。SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了。

8、MultipartResolver
initMultipartResolver(context),用于处理上传请求。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调
用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。

9、FlashMapManager
initFlashMapManager(context),用来管理FlashMap的,FlashMap主要用在redirect中传递参数。

6、Spring Boot 自动配置原理?

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDyEj3A6-1637023656136)(总结最终版.assets/image-20211102090115204.png)]

7、如何理解 Spring Boot 中的 Starter

使用spring + springmvc使用,如果需要引入mybatis等框架,需要到xml中定义mybatis需要的bean

starter就是定义一个starter的jar包,写一个@Configuration配置类、将这些bean定义在里面,然后在starter包的META-INF/spring.factories中写入该配置类,springboot会按照约定来加载该配置类

开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了,比如mybatis-spring-boot–starter,springboot-starter-redis

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

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

9、SpringBoot常用注解及底层实现

核心注解:springBootApplication,这个注解里面有三个注解:

1.@springbootConfiguration

用来代替 applicationContext.xml 配置文件,所有这个配置文件里面能做到的事情都可以通过这个注解所在类来进行注册。

2.@ComponentScan

用来代替配置文件中的 component-scan 配置,开启组件扫描,即自动扫描包路径下的 @Component 注解进行注册 bean 实例到 context 中。

3.@EnableAutoConfiguration

用来提供自动配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fRzEzeaY-1637023656138)(总结最终版.assets/image-20211102174319286.png)]

@Component:将java类标记为bean,是任何spring管理组件的通用构造型,spring的组件扫描机制可以将其拉入应用程序环境中

@Controller 控制层:将一个类标记为jspring web MVC控制器,标有它的bean会自动导入到IOC容器中

@Respository 数据访问层:将DAO导入IOC容器中

@Service 业务逻辑层:

@Autowired :自动注入 更细粒度控制某个属性的注入,默认按类型注入,多个则按照名字注入,不需要额外提供get,set方法

@Resource:属性自动注入;默认按名字匹配

七、Mybatis

1、SpringBoot整合Mybatis的配置文件

server:
  port: 8080
 
spring:
  datasource:
    username: root
    password: 1234
    url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.jdbc.Driver
 
#mybatis的相关配置
mybatis:
  #mapper配置文件
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.zhg.demo.mybatis.entity
  #开启驼峰命名
  configuration:
    map-underscore-to-camel-case: true

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

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

3、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 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库。

4、MyBatis 与Hibernate 有哪些不同?

SQL 和 ORM 的争论,永远都不会终止

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

开发工作量的对比:
Hibernate和MyBatis都有相应的代码生成工具。可以生成简单基本的DAO层方法。针对高级查询,Mybatis需要手动编写SQL语句,以及ResultMap。而Hibernate有良好的映射机制,开发者无需关心SQL的生成与结果映射,可以更专注于业务流程

sql优化方面:
Hibernate的查询会将表中的所有字段查询出来,这一点会有性能消耗。Hibernate也可以自己写SQL来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。而Mybatis的SQL是手动编写的,所以可以按需求指定查询的字段。
Hibernate HQL语句的调优需要将SQL打印出来,而Hibernate的SQL被很多人嫌弃因为太丑了。MyBatis的SQL是自己手动写的所以调整方便。但Hibernate具有自己的日志统计。Mybatis本身不带日志统计,使用Log4j进行日志记录。

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

缓存机制对比:
相同点:都可以实现自己的缓存或使用其他第三方缓存方案,创建适配器来完全覆盖缓存行为。
不同点:Hibernate的二级缓存配置在SessionFactory生成的配置文件中进行详细配置,然后再在具体的表-对象映射中配置是哪种缓存。
MyBatis的二级缓存配置都是在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且Mybatis可以在命名空间中共享相同的缓存配置和实例,通过Cache-ref来实现。
两者比较:因为Hibernate对查询对象有着良好的管理机制,用户无需关心SQL。所以在使用二级缓存
时如果出现脏数据,系统会报出错误并提示。而MyBatis在这一方面,使用二级缓存时需要特别小心。如果不能完全确定数据更新操作的波及范围,避免Cache的盲目使用。否则,脏数据的出现会给系统的正常运行带来很大的隐患。

Hibernate功能强大,数据库无关性好,O/R映射能力强,如果你对Hibernate相当精通,而且对Hibernate进行了适当的封装,那么你的项目整个持久层代码会相当简单,需要写的代码很少,开发速度很快,非常爽。
Hibernate的缺点就是学习门槛不低,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡取得平衡,以及怎样用好Hibernate方面需要你的经验和能力都很强才行。
iBATIS入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。iBATIS的缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。

5、简述 Mybatis 的插件运行原理,如何编写一个插件。

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

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

6、Xml映射文件中,除了常见的select/Inserct/update/delete标签之外,还有那些标签?

  • ,,,,,

  • 动态sql标签:

7、最佳实践中,通常一个Xml映射文件,都会when写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

  • Dao接口的工作原理是JDK动态代理,MyBatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

  • Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。

8、Mybatis是如何进行分页的?分页插件的原理是什么?

  1. Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分离,而非物理分页;
  2. 直接在sql内直接书写limit进行分页;
  3. 使用分页插件来完成物理分页。

9、Mybatis动态sql是做什么?都有哪些动态sql?能简述一下动态sql的执行原理不?

  • Mybatis动态sql可以让我们在xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签
  • 其执行原理为,使用OGNL(Object Graph Navigation Language,对象导航图语言)从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。

10、Mybatis是如何将sql执行结果封装为目标对象并返回的?都有那些映射形式?

  • 第一种是用标签,逐一定义列名和对象属性名之间的映射关系。

  • 第二种是使 用sql列的别名功能,将列名书写为对象属性名。

  • 有了列名和属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

11、Mybatis能执行一对一、一对多的关联查询吗?

  • 有联合查询和嵌套查询。
  • 联合查询是几个表联合查询,只查询一次,通过在resultMap里面的association,collection节点配置一对一,一对多的类就可以完成
  • 嵌套查询是先查一个表,根据这个表里面的结果的外键id,去找另外一个表里面查询数据,也是通过配置association,collection,但另外一个表的查询通过select节点配置。

12、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

  • Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的是一对一,collection值的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=truefalse。
  • 它的原理是,使用CGLIB创建目标的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB.getName(),拦截器invoke()方法a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。

13、Mybatis的XML映射文件中,不同的XML映射文件,id是否可以重复?

  • 不同的XML映射文件,如果配置了Namespace,那么Id可以重复;如果没有配置namespace,那么Id不能重复;
  • 原因就是namespace+id是Map的key使用的,如果没有Namespace,就剩下Id,那么id重复会导致数据互相覆盖。

14、Mybatis都有哪些Executor执行器?他们之间的区别是什么?

  • 有三种:SimpleExecutor、ReuseExector、BatchExector。
  • SimpleExecutor(默认的):每执行一次update或select,就会开启Statement对象,用完立刻关闭Statement对象。
  • ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供一次使用。简言之,重复使用Statement对象。
  • BatchExecutor:执行Update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch),等待统一执行(executorBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理,与JDBC批处理相同。

15、Mybatis中如何制定使用哪一种Executor执行器?

在Mybatis配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给DefalutSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数。

16、Mybatis是否可以映射Enum枚举类?

Mybatis可以映射枚举类,不但可以映射枚举类,Mybatis可以映射任何对象到表的一列上。映射方法为自定义一个TypeHandler,实现TypeHandler的setParameter()和getResult()接口方法。TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParameter()和getResult()两个方法,分别代表设置sql问号占位符参数和获取列查询结果。

17、Mybatis映射文件中,如果A标签通过include引用了B标签的内容,请问B标签的内容是否定义在A标签之后,还是必须定义为A之前?

虽然Mybatis解析Xml映射文件是按顺序接续的,但是,被引用的B标签依然可以定义在任何地方,Mybatis都可以正确识别。

原理是,当我A包含了B的时候,Mybatis解析到A的时候,发现B还没有,会将A设置为未解析状态,然后继续解析下面的标签,待所有标签都解析完全后,再去解析一次未解析的标签。

18、Mybatis与Spring dataJpa的区别

  • SprigData JPA可以理解为对Jpa规范再次封装抽象,底层还是使用hibernate框架的JPA技术实现。
  • JPA默认使用Hibernate技术实现,所以,一般使用Springdata JPA也就是hibernate。
  • Hibernate是一个开放源代码的对象关系映射框架,他对JDBC进行非常轻量级的对象封装。它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,使得Java成员可以随心所欲的使用对象编程思维来操纵数据库。
  • Mybatis是一款优秀的持久层框架,它支持定制化SQL、储存过程以及高级映射。Mybatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。Mybatis可以使用简单的XML或者注解来配置和映射原生信息,将接口和Java的entity对象映射成数据库中的记录。
  • 总的来说,Mybatis可以进行更细致的SQL优化,查询必要的字段,但是需要维护SQL和查询结果集的映射。数据库的移植性较差,针对不同的数据库编写不同的SQL。Hibernate对数据库提供了较为完整的封装,封装了基本的DAO层操作,有较好的数据库移植性,但是学习周期长,开发难度大于Mybatis。

八、Mysql

1、查询语句

select *** from 表名

[ left / right /inner join 表] 联合查询

[ where] 条件

[ group by ] 分组

[ having] 筛选(对分组统计后的结果再筛选)

[ order by] 排序 arc 升序 desc降序

[ limit] 起始值,每页多少数据 分页

查询:

条件 where sdept = ‘计算机’;

范围查询 where age between 20 and 30

集合查询 where sdept in (‘信息管理系’,‘计算机系’)

模糊查询 like / not like _一个字符 %零到多个字符 [] 任意匹配一个 [^]不匹配任意一个

​ 查询30% where field like ‘%30!%%’ escape ‘!’

分组查询 查询选课数超过2的学号、选课数 ,成绩

select sno , count(*),avg(sage)  from sc group by sno having count(*)>3

关键字:

数据库: create drop use show describe

表: create alter (rename重命名,add增加字段,modify改约束,change字段重命名,drop删字段),drop

​ insert into ,update set ,delete from ,truncate ,distinct(去重) , count(*) 行数 ,

​ 权限grant,revoke

where和having的区别

where 子句的作用是在对查询结果进行分组前,将不符合where条件的行去掉,即在分组之前过滤数据,条件中不能包含聚组函数,使用where条件显示特定的行。
having 子句的作用是筛选满足条件的组,即在分组之后过滤数据,条件中经常包含聚组函数,使用having 条件显示特定的组,也可以使用多个分组标准进行分组。

2、视图

视图的优点

视图是一个虚拟表,是存储在数据库中的查询 SQL 语句,视图只是一个逻辑,具体结果在引用视图时动态生成。

优点:① 具有安全性,可以进行权限控制,创建只读视图,公开给特定用户。② 可以简化复杂的查询,保存其逻辑。

3、存储引擎

InnoDB

① MySQL5.1 开始的默认引擎,最大的优点是支持事务和外键,InnoDB 的性能和自动崩溃恢复特性使它在非事务型需求中也很流行,一般应该优先考虑使用 InnoDB。② 底层存储结构是 B+ 树,每个节点都对应 InnoDB 的一个页。非叶子节点只有 key 值,叶子节点包含完整的数据。③ 支持行锁,采用 MVCC 支持高并发,实现了四个标准的隔离级别,默认级别是可重复读,通过间隙锁防止幻读。④ 基于聚簇索引,对主键查询有很高的性能。⑤ 内部做了很多优化,例如加速读操作的自适应哈希索引、加速插入操作的缓冲区等。

MyISAM

① MySQL5.1 及之前的默认引擎,提供的特性包括全文索引、空间索引等,不支持事务、行锁和外键。② 最大的缺陷是崩溃后无法恢复,在插入和更新数据时需要锁定整张表,效率低。③ 对于只读的数据或者表比较小、可以忍受修复操作的情况可以使用 MyISAM。

Memory

① 如果需要快速访问数据且这些数据不会被修改,重启以后丢失也没有关系,可以使用 Memory 表。② 数据保存在内存,不需要磁盘 IO,表的结构在重启后会保留,数据会丢失。③ 支持哈希索引,查找速度快。④ 使用表锁,并发性能低。

4、内连接、左连接和外连接

类型 含义
左外连接 以左表为主表,可以查询左表存在而右表为 null 的记录。
右外连接 以右表为主表,可以查询右表存在而左表为 null 的记录。
内连接 查询左右表同时满足条件的记录,两边都不可为 null。

5、数据库范式

范式是数据库设计规范,范式越高则数据库冗余越小,但查询也更复杂,一般只需满足第三范式。
范式 含义
第一范式 每列都是不可再分的数据单元。
第二范式 在第一范式的基础上消除部分依赖,非主键列完全依赖于主键列。
第三范式 在第二范式的基础上消除传递依赖,非主键列只依赖于主键列。

6、游标是什么

游标是处理数据的一种方法,为了查看或者处理结果集中的数据,游标提供了在结果集中一次一行或者多行前进或向后浏览数据的能力。可以把游标当作一个指针,它可以指定结果中的任何位置,然后允许用户对指定位置的数据进行处理。

7、MySQL 有哪些聚合函数

① max 求最大值。② min 求最小值。③ count 统计数量。④ avg 求平均值。⑤ sum 求和。

6、索引建立的规范

控制数量:索引越多代价越高,对于 DML 频繁的表,索引过多会导致很高的维护代价。

使用短索引:假如构成索引的字段长度比较短,那么在储块内就可以存储更多的索引,提升访问索引的 IO 效率。

建立索引:对查询频次较高且数据量比较大的表建立索引。如果 WHERE 子句中的组合比较多,应当挑选最常用、过滤效果最好的列的组合。业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。

使用前缀索引:对于 BLOB、TEXT 或很长的 VARCHAR 列必须使用前缀索引,MySQL 不允许索引这些列的完整长度。

合适的索引顺序:当不需要考虑排序和分组时,将选择性最高的列放在前面。索引的选择性是指不重复的索引值和数据表的记录总数之比,索引的选择性越高则查询效率越高。

删除重复索引:MySQL 允许在相同列上创建多个索引,重复索引需要单独维护。

7、索引的基本原理

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

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

8、索引设计的原则?

查询更快、占用空间更小

  1. 适合索引的列是出现在where子句中的列,或者连接子句中指定的列

  2. 基数较小的表,索引效果较差,没有必要在此列建立索引

  3. 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间,
    如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。

  4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进
    行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

  5. 定义有外键的数据列一定要建立索引。

  6. 更新频繁字段不适合创建索引

  7. 若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)

  8. 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修
    改原来的索引即可。

  9. 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

  10. 对于定义为text、image和bit的数据类型的列不要建立索引。

9、索引失效的情况

① 隐式类型转换,常见情况是在 SQL 的 WHERE 条件中字段类型为字符串,其值为数值,如果没有加引号那么 MySQL 不会使用索引。
② 如果条件中 OR 只有部分列使用了索引,索引会失效。
③ 执行 LIKE 操作时,最左匹配会被转换为比较操作,但如果以通配符开头,存储引擎就无法做比较,。索引失效
④ 如果查询中的列不是独立的,则 MySQL 不会使用索引。独立的列是指索引列不能是表达式的一部分,也不能是函数的参数。
⑤ 对于多个范围条件查询,MySQL 无法使用第一个范围列后面的其他索引列,对于多个等值查询则没有这种限制。
⑥ 如果 MySQL 判断全表扫描比使用索引查询更快,则不会使用索引。

10、聚簇索引是什么

聚簇索引不是一种索引类型,而是一种数据存储方式。InnoDB 的聚簇索引实际上在同一个结构中保存了 B 树索引和数据行。当表有聚簇索引时,它的行数据实际上存放在索引的叶子页中,由于无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。

优点:可以把相关数据保存在一起;将索引和数据保存在同一个 B 树中,获取数据比非聚簇索引要更快。

缺点:如果数据全部在内存中会失去优势;更新代价高,强制每个被更新的行移动到新位置;插入行或主键更新时,可能导致页分裂,占用更多磁盘空间。

1、mysql聚簇和非聚簇索引的区别

都是B+树的数据结构

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

非聚簇索引:叶子节点不存储数据、存储的是数据行地址,也就是说根据索引查找到数据行的位置再取磁盘查找数据,这个就有点类似一本树的目录,比如我们要找第三章第一节,那我们先在这个目录里面找,找到对应的页码后再去对应的页码看文章。

聚簇索引的优缺点:
优势:
1、查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率要高
2、聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的
3、聚簇索引适合用在排序的场合,非聚簇索引不适合
劣势:
1、维护索引很昂贵,特别是插入新行或者主键被更新导至要分页(page split)的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZE TABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片
2、表因为使用UUId(随机ID)作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更慢,所以建议使用int的auto_increment作为主键
3、如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值;过长的主键值,会导致非叶子节点占用占用更多的物理空间

InnoDB中一定有主键,主键一定是聚簇索引,不手动设置、则会使用unique索引,没有unique索引,则会使用数据库内部的一个行的隐藏id来当作主键索引。在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值

MyISM使用的是非聚簇索引,没有聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。

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

2、mysql索引的数据结构,各自优劣

索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择B+Tree索引。
B+树:
B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互链接。在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库、文件系统等场景。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IACuaUk9-1637023656143)(总结最终版.assets/image-20211102091059721.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UFzH8BFc-1637023656144)(总结最终版.assets/image-20211102091124018.png)]

如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;前提是键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;

如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;

哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);

哈希索引也不支持多列联合索引的最左匹配规则;

B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大;

在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在哈希碰撞问题。

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

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

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

4、sql耗时?慢查询吗?怎么优化?

在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?

所以优化也是针对这三个方向来的,
首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

4、查询优化

① 避免全表扫描:考虑在 WHERE 和 ORDER BY 涉及的列上建立索引,IN 和 NOT IN 也要慎用,尽量用 BETWEEN 取代。

② 优化 COUNT:某些业务不要求完全精确的 COUNT 值,此时可以使用近似值来代替,EXPLAIN 估算的行数就是一个不错的近似值。

③ 避免子查询:在 MySQL5.5 及以下版本避免子查询,因为执行器会先执行外部的 SQL 再执行内部的 SQL,可以用关联查询代替。

④ 禁止排序:当查询使用 GROUP BY 时,结果集默认会按照分组字段排序,如果不关心顺序,可以使用 ORDER BY NULL 禁止排序。

⑤ 优化分页:从上一次取数据的位置开始扫描,避免使用 OFFSET。

⑥ 优化 UNION:MySQL 通过创建并填充临时表的方式来执行 UNION 查询,除非确实需要消除重复的行,否则使用 UNION ALL,如果没有 ALL 关键字,MySQL 会给临时表加上 DISTINCT 选项,对整个临时表的数据做唯一性检查,代价非常高。

⑦ 使用用户自定义变量:用户自定义变量是一个用来存储内容的临时容器,在连接 MySQL 的整个过程中都存在,可以在任何可以使用表达式的地方使用自定义变量,避免重复查询刚刚更新过的数据。

5、锁的类型有哪些

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

  • 共享锁(Share Lock)
共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
  • 排他锁(eXclusive Lock)
排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。
  • 表锁
表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问;
特点: 粒度大,加锁简单,容易冲突;
  • 行锁
行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问;
特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高;
  • 记录锁(Record Lock)
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。
精准条件命中,并且命中的条件字段是唯一索引
加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。
  • 页锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
  • 间隙锁(Gap Lock)
属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。
范围查询并且查询未命中记录,查询条件必须命中索引、间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。
触发条件:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生如下图的问题,在同一个事务里,A事务的两次查询出的结果会不一样。
比如表里面的数据ID 为 1,4,5,7,10 ,那么会形成以下几个间隙区间,-n-1区间,1-4区间,7-10区间,10-n区间 (-n代表负无穷大,n代表正无穷大)
  • 临建锁(Next-Key Lock)
也属于行锁的一种,并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住
触发条件:范围查询并命中,查询命中了索引。
结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。

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

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

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

Record lock:单个行记录上的锁
Gap lock:间隙锁,锁定一个范围,不包括记录本身
Next-key lock:record+gap 锁定一个范围,包含记录本身
相关知识点:

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

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

事务基本特性ACID分别是:

原子性指的是一个事务中的操作要么全部成功,要么全部失败。

一致性指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证

隔离性指的是一个事务的修改在最终提交前,对其他事务是不可见的。

持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中。

隔离性有4个隔离级别,分别是:

  • read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
    用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。
  • read commit 读已提交,两次读取结果不一致,叫做不可重复读。
    不可重复读解决了脏读的问题,他只会读取已经提交的事务。用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。
  • repeatable read 可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。
  • serializable 串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。

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

不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。

幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

7、ACID靠什么保证的?

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

InnoDB redo log 写盘,InnoDB 事务进入 prepare 状态。
如果前面 prepare 成功,binlog 写盘,再继续将事务日志持久化到 binlog,如果持久化成功,那么InnoDB 事务则进入 commit 状态(在 redo log 里面写一个 commit 记录)

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

8、什么是MVCC

多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链

MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lw9woAN5-1637023656147)(总结最终版.assets/image-20211102092346929.png)]

开始事务时创建readview,readView维护当前活动的事务id,即未提交的事务id,排序生成一个数组访问数据,获取数据中的事务id(获取的是事务id最大的记录),对比readview:InnoDB redo log 写盘,InnoDB 事务进入 prepare 状态。

如果前面 prepare 成功,binlog 写盘,再继续将事务日志持久化到 binlog,如果持久化成功,那么InnoDB 事务则进入 commit 状态(在 redo log 里面写一个 commit 记录)

如果在readview的左边(比readview都小),可以访问(在左边意味着该事务已经提交)

如果在readview的右边(比readview都大)或者就在readview中,不可以访问,获取roll_pointer,取上一版本重新对比(在右边意味着,该事务在readview生成之后出现,在readview中意味着该事务还未提交)

已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在
第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。

这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同
实现不同的隔离级别。

9、MVCC解决的问题是什么?

​ 数据库并发场景有三种,分别为:

​ 1、读读:不存在任何问题,也不需要并发控制

​ 2、读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读

​ 3、写写:有线程安全问题,可能存在更新丢失问题

​ MVCC是一种用来解决读写冲突的无锁并发控制,也就是为事务分配单项增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照,所以MVCC可以为数据库解决一下问题:

​ 1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能

​ 2、解决脏读、幻读、不可重复读等事务隔离问题,但是不能解决更新丢失问题

10、大表优化

  • 限定数据的范围:禁止定义没有任何限制数据范围条件的查询语句;

  • 读/写分离:定义主从数据库,主数据库写,从数据库读

  • 垂直分区:根据数据库里面数据表的相关性进行拆分,主键会在多表出现,造成冗余。例如拆分用户登录信息和个人信息,每张表的主键都是用户id

    • 优点:lock数变少,I/O操作会减少,简化表结构,易维护
    • 缺点:主键会冗余,会引起join操作(可以在应用层解决),表多会使事务变得复杂
  • 水平分区:当行记录过多时,把行数据分别存储到其他表中,达到分布式目的

    • 数据库分片的两种方式:
      • 客户端代理:分片逻辑在应用端,封装在jar包,通过修改或封装JDBC实现
      • 中间件代理:在应用和数据中间加一个代理层,分片逻辑在中间服务器维护
    • 缺点:分片事务难以解决,会带来逻辑、部署和运维的各种复杂度

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

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

union
排序字段是唯一索引:

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

12、什么是mysql的主从复制?

​ MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表。

13、mysql为什么需要主从同步?

1、在业务复杂的系统中,有这么一个情景,有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作。

2、做数据的热备

3、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。

14.mysql主从同步原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DnQ1kOP1-1637023656148)(file://C:\Users\xiaoting\Desktop%E5%BD%93%E5%89%8D%E9%A2%98%E7%9B%AE%E5%92%8C%E7%AD%94%E6%A1%88\images%E4%B8%BB%E4%BB%8E%E5%8E%9F%E7%90%86.png?lastModify=1636427850)]

mysql主从同步的过程:

Mysql的主从复制中主要有三个线程: 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确认给主库,主库收到至
少一个从库的确认就认为写操作完成。

15、简述MyISAM和InnoDB的区别

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

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

16、mysql执行计划怎么看

执行计划就是sql的执行查询的顺序,以及如何使用索引查询,返回的结果集的行数
EXPLAIN SELECT * from A where X=? and Y=?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0iyEVIVz-1637023656150)(总结最终版.assets/image-20211102093122724.png)]

1。id :是一个有顺序的编号,是查询的顺序号,有几个 select 就显示几行。id的顺序是按 select 出现的顺序增长的。id列的值越大执行优先级越高越先执行,id列的值相同则从上往下执行,id列的值为NULL最后执行。
2。selectType 表示查询中每个select子句的类型
SIMPLE: 表示此查询不包含 UNION 查询或子查询
PRIMARY: 表示此查询是最外层的查询(包含子查询)
SUBQUERY: 子查询中的第一个 SELECT
UNION: 表示此查询是 UNION 的第二或随后的查询
DEPENDENT UNION: UNION 中的第二个或后面的查询语句, 取决于外面的查询
UNION RESULT, UNION 的结果
DEPENDENT SUBQUERY: 子查询中的第一个 SELECT, 取决于外面的查询. 即子查询依赖于外层查询的结果.
DERIVED:衍生,表示导出表的SELECT(FROM子句的子查询)

3.table:表示该语句查询的表
4.type:优化sql的重要字段,也是我们判断sql性能和优化程度重要指标。他的取值类型范围:
const:通过索引一次命中,匹配一行数据
system: 表中只有一行记录,相当于系统表;
eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配
ref: 非唯一性索引扫描,返回匹配某个值的所有
range: 只检索给定范围的行,使用一个索引来选择行,一般用于between、<、>;
index: 只遍历索引树;
ALL: 表示全表扫描,这个类型的查询是性能最差的查询之一。 那么基本就是随着表的数量增多,执行效率越慢

执行效率:
ALL < index < range< ref < eq_ref < const < system。最好是避免ALL和index

5.possible_keys:它表示Mysql在执行该sql语句的时候,可能用到的索引信息,仅仅是可能,实际不一定会到。
6.key:此字段是 mysql 在当前查询时所真正使用到的索引。 他是possible_keys的子集
7.key_len:表示查询优化器使用了索引的字节数,这个字段可以评估组合索引是否完全被使用,这也是我们优化sql时,评估索引的重要指标
9.rows:mysql 查询优化器根据统计信息,估算该sql返回结果集需要扫描读取的行数,这个值相关重要,索引优化之后,扫描读取的行数越多,说明索引设置不对,或者字段传入的类型之类的问题,说明要优化空间越大
10.filtered:返回结果的行占需要读到的行(rows列的值)的百分比,就是百分比越高,说明需要查询到数据越准确, 百分比越小,说明查询到的数据量大,而结果集很少
11.extra
using filesort :表示 mysql 对结果集进行外部排序,不能通过索引顺序达到排序效果。一般有using filesort都建议优化去掉,因为这样的查询 cpu 资源消耗大,延时大。
using index:覆盖索引扫描,表示查询在索引树中就可查找所需数据,不用扫描表数据文件,往往说明性能不错
using temporary:查询有使用临时表, 一般出现于排序,分组和多表 join 的情况,查询效率不高,建议优化。
using where :sql使用了where过滤,效率较高。

九、数据结构

1.排序

排序方法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 稳定性
插入排序 O(N2) O(N2) O(N) O(1) 稳定
希尔排序 O(N1.3) O(N2) O(N) O(1) 不稳定
选择排序 O(N2) O(N2) O(N2) O(1) 不稳定
堆排序 O(NlogN) O(NlogN) O(NlogN) O(1) 不稳定
冒泡排序 O(N2) O(N2) O(N) O(1) 稳定
快速排序 O(NlogN) O(N2) O(NlogN) O(NlogN) 不稳定
归并排序 O(NlogN) O(NlogN) O(NlogN) O(N) 稳定
计数排序 O(n+k) O(n+k) O(n+k) O(n+k) 稳定
桶排序 O(n+k) O(n2) O(n) O(n+k) 稳定
基数排序 O(n*k) O(n*k) O(n*k) O(n+k) 稳定

十、计算机网络

1、网络七层、五层、四层架构

**七层:**应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

**五层:**应用层、传输层、网络层、数据链路层、物理层

**四层:**应用层、传输层、网络层、网际接口层

2、五层分层介绍

应用层:

  • 任务:通过应用进程间的交互来完成特点网络应用。
  • 应用层协议:定义不同应用进程之间交互和通信的规则。
  • 协议:DNS域名系统、HTTP/HTTPS万维网协议、SMTP邮件协议
  • 数据单位:报文

传输层:

  • 任务:为不同应用层进程之间的通信提供通用的数据传输服务
  • 功能:复用和分用
    • 复用:不同应用进程可以同时使用传输服务协议
    • 分用:传输层收集的信息分别交付于上面网络层相应的进程
  • 协议:TCP、UDP

网络层:

  • 原因:因为两个计算机节点之前存在多条数据链路。
  • 任务:选择合适、可靠的网间路由和交换节点,及时传输数据
  • 数据单位:IP数据报
  • 网路协议是由无连接的网际协议和许多路由协议组成,因此也叫网际层或IP层

为什么有了传输层,还要有网络层?

应用通信和设备通信不一样,应用通信是解决怎样将应用数据合适、安全地到达目的地;而设备通信是当一个封包到达路由器,如何及时传送到另一个设备。

数据链路层:

  • 原因:数据是在两个节点之间的链路进行传输的,因此需要链路协议
  • 任务:将IP数据报或者用户信息封装成帧,利用链路协议在链路上进行传输数据;控制信息使接收端知道从哪个帧开始读取数据,从哪个帧结束读取;
  • 数据单位:帧

物理层:

  • 任务:实现相邻计算机之间比特流在物理层的透明方式传输
  • 透明方式:尽可能忽略具体的传输介质和物理设备的差异性
  • 数据单位:比特

3、三次握手

具体过程:

第一次:客户端发送请求连接报文,将SYN(SYNchronization,同步序列编号)置为1,将自己一个初始序列x存入TCP的序号段;

第二次:服务器接受到请求报文后,为其分配TCP缓存和变量,向客户端发送一个确认收到请求连接的确认报文,将SYN置为1,ACK置为1,确认号设置为x+1,并将自己的序列号y存入TCP序号段中;

第三次:客户端收到确认报文也会为其分配缓存和变量,向服务器发送确认报文,将SYN置为0,ACK置为1,确认好置为y+1。此时可以携带数据

为什么要三次握手?

为了确保发送方和接收方之间的发送和接受都是正常的

为什么要传回SYN?

为了确保接收端和发送端的通信通道正常,接收端告诉发送端,我收到的信息是你发送的信息。

为什么有了SYN,还要有ACK?

SYN只能确认发送端到接收端的通信通道,而接收端和发送端的通信通道需要ACK确认

第三次握手失败,怎么办?

服务器会重新发送第二次握手请求,要求客户端重传第三次握手,默认使5次,超过了默认次数,服务器会关闭连接

什么是SYN泛洪攻击?服务器怎么防范?

服务器在收到客户端发送的第一次请求报文时,会为其分配缓存,这就使黑客利用大量假的ip报文去建立安全连接,导致服务器不断分配缓存,造成资源消耗殆尽。

服务器会建立SYN Session,在收到第一次请求报文不会为其分配缓存,只有完成了三次握手后,才建立具有套接字的安全连接。

4、四次挥手

具体过程:

第一次:客户端发送。发送一个关闭连接的请求,FIN置为1,序列号随机置为u,发送完毕后,进入FIN-waiting状态;

第二次:服务器发送。接受到请求后,发送一个确认接受到请求的报文,表明自己收到请求,但是还没有准备好关闭连接,其中ACK置为1,确认号为u+1,发送完毕后,进入Close-wait状态;客户端在收到确认包后,进入FIN-wait2状态;

第三次:服务器发送。当服务器准备好关闭连接,向客户端发送一个确认关闭连接的报文,将FIN置为1,发送完毕后,服务器进入Last-ACK状态;

第四次:客户端发送。客户端发送一个确认报文,将ACK置为1,并进入time-wait状态,等待可能出现的重传报文信息;服务器收到确认报文后,进入close状态

当客户端等待了2MSL时间段时,没有任何消息,则进入close状态。

为什么会有time-wait状态?

为了确保服务器是否收到客户端发送的确认报文,若没有收到,则服务器会重新进行第三次挥手,客户端收到报文后,知道之前失败了,会重新发送了ACK确认报文。

为什么三次握手,四次放手?

是因为,服务器在收到客户端请求关闭连接时,一些应用进程可能还没有结束,不能立刻关闭,因此,需要告诉客户端一个等待关闭通知的报文。

为什么需要2MSL时间客户端才关闭连接?

2MSL是一个发送消息和一个回复所需的最大时间

5、TCP和UDP区别

TCP:传输控制协议

UDP:用户数据报协议

TCP UDP
有连接 无连接
只能进行一对一进行传输 一对一、一对多、多对一、多对多传输
面向字节流 直接将应用层数据报封成包进行传输
有流量控制和拥塞控制(安全可靠传输) 没有流量控制和拥塞控制(不可靠传输)
首部字节最小为20,最大为60字节 首部较小,只有8字节

6、TCP如何保证可靠传输

  1. 分割数据块。会将应用层数据分割成TCP认为最适合传输的数据块;
  2. 编号排序。TCP每发送一个数据包都会进行编号,接受方进行排序,然后将有序的数据包发送给应用层;
  3. 校验和。确保数据在传输过程中没有发生变化,若有变化,则丢弃;
  4. 丢弃重复。TCP接收端会丢弃重复收到的数据
  5. 流量控制。TCP的每一方都有一个固定的缓存池空间,接收端允许发送端发送接收端能够容纳的数据
  6. 拥塞控制。当网络拥塞时,减少数据传输量
  7. ARQ协议。TCP每发送一组数据,会等待确认帧,若没有收到,会重传该组数据;
  8. 超时重传。TCP每发送一段数据时,会启动一个定时器,如果超过定时器时间,会重新传数据

7、ARQ协议

自动重传请求,数据链路层和传输层常用的协议之一,通过确认和超时实现可靠传输。发送方在规定的时间内未收到确认帧,则对数据进行重传

8、滑动窗口和流量控制

TCP通过滑动窗口进行流量控制。流量控制通过控制发送方发送数据的速率,来确保接收方能够及时接受数据。接收方发送的确认报文中的窗口字段可以控制发送方窗口的大小,从而影响发送方的发送速率。

9、拥塞控制

  • 在某段时间内,对网络某一资源的需求超过了该资源提供的可用部分,此时网络性能会变坏。拥塞控制就是为了防止网络中注入过多的数据,导致网络中的路由器和数据链路过载现象发生。
  • 发送方要维持一个拥塞窗口的状态变量,值随网络拥塞程度进行动态变化,要求发送方的发送窗口大小为拥塞窗口和接收方窗口中的较小值
  • TCP拥塞控制有四种算法:慢开始、拥塞避免、快重传和快恢复
    • 慢开始:发送窗口由小变大
    • 拥塞避免:拥塞窗口由小变大
    • 快重传和快恢复:发送发接受到的确认帧信息有误,客户端利用FRR(快重传和快恢复算法)会要求服务器重复确认,当收到三个重复报文,就认为数据丢失了,不需要等到定时器结束,立刻重新传输数据

10、在浏览器输入url地址->显示主页的过程

  1. DNS域名解析
  2. TCP连接
  3. HTTP请求
  4. 服务器处理请求并返回HTTP信息
  5. 浏览器解析并渲染视图,展示结果
  6. 关闭连接

11、状态码

1xxx 接受的请求正在处理
2xxx 请求正常处理完毕
3xxx 需要附加操作,才能完成请求
4xxx 服务器无法识别处理请求(客户端出错)
5xxx 服务器处理请求出错(服务器出错)

12、HTTP长连接和短连接

短连接:http1.0默认,每进行一个http操作,就会进行一个客户端和服务器之间的连接,消耗资源和时间

长连接:http1.1起默认,在一段时间内,浏览器对服务器的http请求会使用一个之前建立好的连接,即使关闭浏览器,下次http请求,依然无需重连,只要在规定的时间内即可。

13、HTTP是不保存状态的协议,如何保存用户状态

  • 使用Session机制,通过服务器端记录用户状态相关信息。服务器给特定的用户创建特定的Session,之后就可以用对应的Session表示这个用户并进行跟踪
  • 保存Session常见方法是内存(使用redis进行保存)和数据库

14、Cookie的作用是什么?与Session的区别

Cookie和Session都是用来记录和跟踪用户的会话方式。

作用:

  • Cookie是通过客户端用来记录用户的信息
  • Cookie可以在浏览器中记录用户一些登录信息,等用户下次登录的时候,会自动帮助用户登录;或者保持登录状态,下次用户访问浏览器的时候,直接显示登录后的页面。(原因:我们会将Token放进Cookie中,下次登陆时,只需要根据Token值来查找用户即可)

区别:

  • Cookie是通过客户端进行记录用户,而Session通过服务器记录用户
  • 单个cookie保存的数据<=4kb,一个站点最多保存20个Cookie;Session没有上限
  • Cookie只能保存ASCII码,session能存储任何类型的数据

15、HTTP各代版本增加的功能

HTTP/0.9:

  • 采用纯文本

HTTP/1.0:

  • 增加head、post请求
  • 引入协议版本号概念

HTTP/1.1:

  • 增加put、delete请求
  • 允许长连接

HTTP/2:

  • 通信进行加密

HTTP/3:

  • 使用UDP的QUIC协议:
    • 多路复用:基于一个TCP连接,当有一个资源丢包,不需要全部重传,只传丢包的资源即可
    • 加密认证的报文:QUIC协议对所有报文进行加密处理
    • 向前纠错:传输的每个数据包都包含其他数据包的信息,当其中一个丢包,会从其他包的冗余数据直接组装,不需要重传。当丢多个包时,只能重传

16、URI和URL

URI:统一资源标识。唯一标识资源

URL:统一资源定位。提供资源的路径。不仅可以唯一标识资源,可以明确定位到资源的位置

17、HTTP和HTTPS的区别

  • 端口号。
    • HTTP端口是80
    • HTTPS端口是443
  • 安全性和资源消耗
    • HTTP传输没有进行加密,在传输中,容易被拦截信息,导致泄露不安全,但是资源消耗较小;
    • HTTPS传输进行了对称性加密,其中的密钥是服务器的证书进行了非对称加密。安全,但是接受到数据信息,需要进行解密,因此要消耗一定的资源

18、对称加密和非对称加密

对称加密:密码只有一个,加密和解密都是同一个密钥

非对称加密:密钥成对出现,加密和解密需要不同的密钥。公钥加密需要私钥解密,私钥加密需要公钥解密,速度较慢

cookie 和 session

① cookie 只能存储 ASCII 码,而 session 可以存储任何类型的数据。

② session 存储在服务器,而 cookie 存储在客户浏览器中,容易被恶意查看。。

③ session 的运行依赖 session id,而 session id 存在 cookie 中,叫做 JSESSIONID。如果浏览器禁用了 cookie ,同时 session 也会失效(可以通过其它方式实现,比如在 url 中传递 session_id)。

TCP 三次握手的过程?为什么要三次握手

初始 A 和 B 均处于 CLOSED 状态,B 创建传输进程控制块 TCB 并进入 LISTEND 状态,监听端口是否收到连接请求。

A 向 B 发送连接请求报文,SYN=1,ACK=0,SYN 不可以携带数据,但要消耗一个序号,发送后 A 进入 SYN-SENT 同步已发送状态。

B 收到 A 的连接请求报文后,进入 SYN-RCVD 同步已接收状态,如果同意建立连接就会发送给 A 一个连接响应报文,SYN=1,ACK=1,ACK 可以携带数据,不携带的话则不消耗序号。

A 收到 B 的确认后还要对该确认再进行一次确认,发送后 A 进入 ESTABLISHED 状态,B 接收到该报文后也进入 ESTABLISHED 状态,客户端会稍早于服务器端建立连接。

三次握手的原因:

从信息对等角度看,AB 分别要确认自己和对方的发送、接收能力均正常。第二次握手后 B 还不能确定自己的发送和 A 的接收能力。

A 的超时连接请求可能会在双方释放连接后到达 B,B 会误以为是 A 发送了新的连接请求,然后创建连接,服务器资源被浪费。

拥塞控制了解嘛

网络中对资源的需求超过可用量的情况就叫拥塞,当吞吐量明显小于理想吞吐量时就出现了轻度拥塞。拥塞控制就是减少注入网络的数据,减轻路由器和链路的负担,这是一个全局性问题,涉及网络中的所有路由器和主机,而流量控制是一个端到端的问题。

TCP 的拥塞控制算法包括了慢启动、拥塞避免和快恢复。慢启动和拥塞避免是 TCP 的强制部分,差异在于对收到的 ACK 做出反应时拥塞窗口增加的方式,慢启动比拥塞避免增加得更快。快恢复是推荐部分,对 TCP 发送方不是必须的。

慢启动:拥塞窗口 cwnd 以一个 MSS 最大报文段开始,每当传输的报文段首次被确认就增加一个 MSS。因此每经过一个 RTT 往返时间,拥塞窗口就会翻倍,发送速率也会翻倍。结束慢启动的情况:① 发生超时事件,发送方将 cwnd 设为 1,重新开始慢启动,并将慢启动阈值设置为 cwnd/2。② 当拥塞窗口达到慢启动阈值时就结束慢启动而进入拥塞避免模式。③ 如果检测到三个冗余的 ACK,TCP 就会执行快重传并进入快恢复状态。

拥塞避免:一旦进入拥塞避免状态,cwnd 值大约是上次拥塞时的 1/2,距离拥塞并不遥远。因此 TCP 不会每经过一个 RTT 就将 cwnd 翻倍,而是较为保守地在每个 RTT 后将 cwnd 加 1。发生超时事件时,拥塞避免和慢启动一样,将 cwnd 设为 1,并将慢启动阈值设置为 cwnd/2。

快恢复:有时个别报文段丢失,但网络中并没有出现拥塞,如果使用慢启动会降低传输效率。这时应该使用快重传来让发送方尽早知道出现了个别分组的丢失,快重传要求接收端不要等待自己发送数据时再捎带确认,而是要立即发送确认。即使收到了乱序的报文段也要立即发出对已收到报文段的重复确认。当发送方连续收到三个冗余 ACK 后就知道出现了报文段丢失的情况,会立即重传并进入快恢复状态。在快恢复中,会调整慢启动阈值为 cwnd/2,并进入拥塞避免状态。

滑动窗口怎么变化的

滑动窗口以字节为单位。发送端有一个发送窗口,窗口中的序号是允许发送的序号,窗口的后沿是已发送且确认的序号,窗口的前沿是不允许发送的序号。窗口的后沿可能不动(没有收到新的确认),也有可能前移(收到了新的确认),但不会后移(不可能撤销已经确认的数据)。窗口的前沿一般是向前的,可能不动(没有收到新的请求或对方的接收窗口变小),也可能收缩(TCP 强烈不建议这么做,因为发送端在收到通知前可能已经发送了很多数据,将产生错误)。

TCP 四次挥手的过程?为什么要四次挥手

当 A 没有要发送的数据时就会向 B 发送终止连接报文,FIN=1,发送后 A 进入 FIN-WAIT-1 状态。

B 收到后发给 A 一个确认报文,A 进入 FIN-WAIT-2 状态,B 进入 CLOSE-WAIT 状态,TCP 进于半关闭状态。

当 B 也准备释放连接时就向 A 发送连接终止报文,FIN=1,重发 ACK=1,之后 B 进入 LAST-ACK 状态。

A 收到后要再进行一次确认,ACK=1,之后进入 TIME-WAIT 状态,等待 2MSL 后进入 CLOSED 状态。B 收到确认后进入 CLOSED 状态。

四次挥手的原因:TCP 是全双工通信,两个方向的连接需要单独断开。

等待 2MSL 的原因:

MSL 是最大报文段寿命,等待 2MSL 可以保证 A 发送的最后一个确认报文被 B 接收,如果该报文丢失,B 会超时重传之前的 FIN+ACK 报文,保证 B 正常进入 CLOSED 状态。

2MSL 后,本连接中的所有报文就都会从网络中消失,防止已失效请求造成异常。

TCP 和 UDP 的区别

① TCP 是面向连接的,发送数据前必须先建立连接,发送某些预备报文段;UDP 无连接,发送数据前不需要建立连接。

② TCP 连接是点对点的,只能是单个发送方和单个接收方之间的连接;UDP 支持一对一、一对多和多对多通信。

③ TCP 提供可靠的交付服务,通过 TCP 传送的数据无差错、不丢失、不重复,按序到达;UDP 使用尽最大努力交付,不保证可靠性,主机不需要维持复杂的连接状态。

④ TCP 是面向字节流的,TCP 不保证接收方的数据块和发送方的数据块具有对应大小的关系,但接收方的字节流必须和发送方的字节流完全一样。应用程序必须有能力识别收到的字节流,把它还原成应用层数据;UDP 面向报文,对应用层报文添加首部后就交付 IP 层。

⑤ TCP 有拥塞控制;UDP 没有拥塞控制,网络拥塞不会降低源主机的发送速率,这对某些实时应用很重要,如视频会议。

HTTP 和 HTTPS

HTTP 超文本传输协议,由客户程序和服务器程序实现,客户程序和服务器程序通过交换 HTTP 报文进行会话。HTTP 定义了这些报文的结构以及报文交换的方式,当用户请求一个 Web 页面时,浏览器向服务器发出对该页面中所包含对象的 HTTP 请求报文,服务器接收请求并返回包含这些对象的 HTTP 响应报文。

HTTP over SSL,在 HTTP 传输上增加了 SSL 安全套接字层,通过机密性、数据完整性、身份鉴别为 HTTP 事务提供安全保证。SSL 会对数据进行加密并把加密数据送往 TCP 套接字,在接收方,SSL 读取 TCP 套接字的数据并解密,把数据交给应用层。HTTPS 采用混合加密机制,使用非对称加密传输对称密钥保证传输安全,使用对称加密保证通信效率。

HTTPS 过程

① 客户发送它支持的算法列表以及一个不重数。不重数就是在协议的生存期只使用一次的数,用于防止重放攻击,每个 TCP 会话使用不同的不重数,可以使加密密钥不同,重放记录无法通过完整性检查。

② 服务器从该列表中选择一种对称加密算法(例如 AES),一种公钥加密算法(例如 RSA)和一种报文鉴别码算法,然后把它的选择、证书,一个不重数返回给客户。

③ 客户通过 CA 提供的公钥验证证书,成功后提取服务器的公钥,生成一个前主密钥 PMS 并发送给服务器。

④ 客户和服务器独立地从 PMS 和不重数中计算出仅用于当前会话的主密钥 MS,然后通过 MS 生成密码和报文鉴别码密钥。此后客户和服务器间发送的所有报文均被加密和鉴别。

十一、操作系统

1、进程与线程

进程:

  • 进程是程序的一次执行过程,是系统运行的基本单位,系统运行一个程序是一个进程创建、运行到消亡的过程。

线程:

  • 线程是处理器调度和执行的基本单位。一个进程包括多个线程,线程共享所在进程的堆和方法区的资源,有自己独立的虚拟栈、本地方法栈和程序计数器,上下文切换开销小,因此叫轻量级进程

2、僵尸进程

  • 运行程序的时候,一个父进程可能会有多个子进程跑,子进程执行完毕后会发送一个exit()信号,父进程没有去处理,导致这个子进程一直在进程表中。

  • 解决:

    • 通过信号机制。子进程退出时向父进程发送sigchild信号,父进程处理sigchild信号。在信号处理函数中调用wait进行处理僵尸进程
    • fork两次。将第二个子进程成为孤儿进程,从而其父进程变为init进程,通过init进程可以处理僵尸进程
    • 找到僵尸进程的父进程,然后执行kill -9,杀掉父进程,使子进程成为孤儿进程

3、孤儿进程

父进程退出后,而它的子进程还在运行,那这些子进程就是孤儿进程

解决方法:

成为孤儿进程后,有init进程对它进行操作,最后孤儿进程在init进程下结束生命周期

4、物理内存与虚拟内存

**物理内存:**硬件实际的内存大小

**虚拟内存:**计算机技术中一种内存管理技术,利用一部分物理内存充当内存,只将运行的部分放入其中,运行完,再与外存接下来需要运行的部分进行交换。

5、Linux命令

5.1、Kill

**命令格式:**kill 【参数】【进程号】

命令参数:

  • -l 信号,如果不加参数,会列出全部的信号名称
  • -a 当处理当前进程时,不限制命令名和进程号的对应关系
  • -p 指定kill命令只打印相关进程的进程号,而不发生任何信号
  • -s 指定发送信号

工作原理:

向linux系统的内核发送一个系统操作信号和某个程序的进程标识号,然后系统内核就可以对标识号进行操作

应用:

  • 强行终止进程,kill -9 进程号
  • 解除linux死锁的问题,杀掉对应的进程
  • 使用命令回收内存。用top命令发现无用的进程,然后杀死

5.2、Linux的硬链接和软链接

**硬链接:**可理解为指向原始文件的inode的指针,系统不为它分配独立的inode和文件。因此,硬链接文件和原始文件是同一个文件,只是名字不同。每当增加一个硬链接,该文件的inode连接数就会增加1;而且只有当该文件的inode连接数为0时,才算彻底将它删除

**软连接:**类似于Windows系统的快捷方式,仅包含所含链接文件的路径名字。

区别:

  • 不能对目录创建硬链接,而可以对目录创建软链接;
  • 不能对不同的文件系统创建硬链接,软链接可以跨文件系统
  • 不能对不存在的文件创建硬链接,可以对不存在的文件创建软链接

有一些任务紧急的进程需要执行,采用什么调度算法?你还知道哪些调度算法?

可以采用优先级调度,使用剥夺式。

① 先来先服务 FCFS,从后备队列选择最先进入的作业,调入内存。

② 短作业优先 SJF,从后备队列选择估计运行时间最短的作业,调入内存。平均等待时间、平均周转时间最少。

③ 优先级调度算法,分为非剥夺式和剥夺式。

④ 高响应比优先算法,综合了 FCFS 和 SJF,同时考虑了每个作业的等待时间和估计的运行时间。

⑤ 时间片轮转算法,遵循先来先服务原则,但是一次只能运行一个固定的时间片。

什么是原语

若干指令组成的程序段,用来实现某个特定功能,具有原子性,在执行过程中不可中断。P 是阻塞原语,将进程从运行态转为阻塞态,直到另一个进程唤醒它;V 是唤醒原语,将被阻塞的进程唤醒。

Linux 中进程有哪些状态

① 可执行状态,正在运行或等待运行。
② 可中断的等待状态。
③ 不可中断的等待状态。
④ 停止状态。
⑤ 终止状态(僵尸进程)。

进程包含什么

① 进程控制块 PCB :进程存在的唯一标识,包括进程描述信息、控制信息、资源分配信息等。
② 程序段:能被进程调度到 CPU 执行的代码。
③ 数据段:进程对应的程序加工处理的原始数据。

如何查看文件?cat 和 more 的区别?

cat、more、less。cat 一次性显示全部文件,more 是以页的形式查看。

压缩命令

① zip/unzip:压缩文件/解压缩,兼容 Linux 与 Windows,可以压缩多个文件或目录。
② gzip/gunzip:压缩文件/解压缩 gzip 文件,压缩单个文件,压缩率相对低,CPU 开销低。
③ xz/unxz:压缩/解压缩 xz 文件,压缩单个文件,压缩率高,时间相对长,解压快,CPU 开销高。

如何查看文件后 10 行

tail - n 10 13.

vim 和 cat 的区别

vim 可以编辑文件内容,cat 只能查看。

列出文件命令?按时间的选项是什么

ls -t

十二、JVM

1、Java内存区域

线程共有的:

  • 方法区
  • 直接内存(非运行时数据区的一部分)

线程私有的:

  • 虚拟机栈
  • 本地方法栈
  • 程序计数器

1.1、程序计数器

  • 字节码解释器通过改变程序计数器的大小来一次读取指令
  • 在当前线程中,保存程序指令执行的位置,方便再次切回线程后继续执行

1.2、Java虚拟栈

  • 它的生命周期和线程相同,描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的
  • Java虚拟机栈是由多个栈帧组成,每个栈帧都有方法索引、输入输出参数、本地变量(局部变量)、引用、父帧和子帧,即调用函数所需要的所有东西。
  • Java虚拟机会出现两种异常:
    • StackOverflowError:如果Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机的最大深度的时候,就会抛出异常
    • OutOfMemoryError:如果Java虚拟机栈的内存大小允许动态扩展,那么线程请求时内存用完了,就会抛出异常
  • 如何调用方法/函数
    • 每次函数调用都会有一个对应的栈帧压入Java栈,函数调用结束后,会有一个栈帧被弹出
    • Java方法有两种返回方式:
      • return方式
      • 抛出异常

1.3、本地方法栈

  • 虚拟机栈是为虚拟机执行Java方法(字节码)服务,而本地方法栈则是为了虚拟机使用到的native方法服务的

  • 在HotSpot虚拟机中和Java虚拟机合二为一

  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,里面包含调用函数所需要的所有东西

  • Native:

    • 凡是带有native关键字的,说明Java的作用范围达不到了,回去调用底层c语言的库
    • 会调用本地方法接口JNI
  • JNI作用:扩展Java的使用,融合不同的编程语言为Java所用

  • 本地方法栈会在内存空间区域专门开辟了一块标记区域:Native Method Stack,登记native方法

1.4、方法区

  • 内容:静态变量、常量、类信息(构造方法、接口定义、Class加载模板)、运行时的常量池
  • 运行时常量池:用于存放编译期生成的各种字面值和符号引用(检查对象是否引用)
  • 调用方法的过程
    • 先将类模板加载
    • 将默认的成员加载到常量池
    • 调用方法
    • 去堆进行赋值操作,如果没有赋值,从常量池获取

1.5、直接内存

  • 直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误。
  • JDK1.4 中新加入的 NIO(New Input/Output)类,引入了一种基于通道缓存区的 I/O 方式, 它可以直接使用 Native 函数库直接分配堆外内存, 然后通过一个存储在 Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。

1.5、堆

  • 堆是线程共享的一片最大区域。存放变量、类、方法

  • Java对象就是在堆中进行创建和回收的。

  • Java 堆是 GC 系统的主要区域。

  • 为了解决频繁创建和回收对系统开销大的问题,引入了JIT(即时编译)逃逸分析

  • 即时编译(JIT)

    • https://blog.csdn.net/shenwansangz/article/details/95601232
    • 当某段代码即将第一次被执行时进行编译,叫“即时编译”
    • Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为**“热点代码”**。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器
  • 检查热点程序

    • 基于采样的热点探测:周期检查每个线程的栈顶方法,经常出现的方法,就是“热点方法”
    • 基于计数器的热点探测(HotSpot虚拟机默认):为每个方法建立计数器,统计执行方法的次数,超过一定的次数就认为它是”热点方法” 。包含两个计数器:(1)方法调用计数器;(2)回边计数器。
  • 逃逸分析(JIT中的一种优化)

    • https://blog.csdn.net/hollis_chuang/article/details/80922794

    • 方法逃逸:当一个对象在方法中被定义后,作为调用传入参数传递到其他方法中

    • 线程逃逸:当一个对象在方法中被定义后,被其他线程访问到,譬如赋值给类变量。

    • 优化:

      • 栈上分配(针对方法逃逸):将不会逃逸的对象分配到栈上,对象随着方法的结束而结束,减少 GC 系统的压力
      • 同步消除(针对线程逃逸):如果其他线程不访问该对象,那么我们可以把同步措施取消掉
      • 标量替换:将对象的一个聚合产物分解成多个基本成员变量。这个方法不是创建对象了,直接创建对象里面的成员变量到栈上分配与读写。
    • -XX:+DoEscapeAnalysis : 表示开启逃逸分析

      -XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析

  • 分代垃圾收集算法

    • 分类:
      • JDK7 以及之前,分为新生代内存(Eden)、老生代(old)、永生代
      • JDK8 版本之后,用元空间代替永生代,元空间(perm)使用的是直接内存。这样做的原因:(1)永久代被 JVM 设定了一个大小限制,而元空间直接使用的直接内存受本机可用内存的限制,但是内存溢出的可能性大大减少。(2)JDK8,合并 HotSpot 和 JRockit 的代码时,JRockit 没有永久代,合并后也没有必要开一个额外的设置。
    • 大部分的情况,对象都会现在 Eden 区域分配,然后进行了一次新生代垃圾回收,如果对象还活着,就进入新生代区域中的幸存区 0 或 1,并且年龄还会+1,当它的年龄增到一定程度(默认15 岁),就会到老年代0。
    • 对象晋升到老年代的年龄阈值:可以通过 -XX:MaxTenuringThreshold来设置。
  • 动态对象年龄判断,如果幸存区中相同年龄的对象所有大小之和超过了幸存区的空间一半,那么大于等于该年龄的对象直接进入老年区,无需等到默认年龄。

  • 堆中容易出现OOM(内存溢出)

    • OutOfMemoryError:GC Overhead Limit Exceeded:当Java花太多时间执行垃圾回收并且只能回收很少的堆空间时,会发生此错误
    • java.lang.OutOfMemoryError:Java heap space:在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误

2、Java对象创建的过程

2.1、类加载检查

虚拟机遇到一条 new 指令的时候,首先去常量池中检查是否该对象的符号引用,并检查该引用是否被加载过、初始化过、解析过。如果没有,就要去执行类加载过程。

2.2、分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定。分配方式有两种:”指针碰撞”和“空闲列表”两种,选择那种分配方式由Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集是否带有压缩整理功能所决定。

  • 指针碰撞:
    • 适用场合:堆内存规整(没有内存碎片)的情况(复制算法,标记压缩算法)
    • 原理:用过的内存全部整合到一边,其中用一个指针来分隔,来了一个新对象,指针往没有用过内存的地方移动。
    • GC 收集器:serial(标记压缩),parallel(serial 的多线程版本)
  • 空闲列表
    • 使用场合:堆内存不规整,有内存碎片(标记清楚算法)
    • 原理:虚拟机会维护一个列表,该列表中会记录那些内存块是可用的,在分配的时候,找一块足够大的内存块来创建对象实例,然后更新列表。
  • 内存分配并发问题
    • 在创建对象中,不允许另外的线程来干扰,通常也有两种方式来保证创建对象是线程安全的
    • **CAS+失败重试:**CAS 是乐观锁的一种实现。乐观锁是指,它每次都假设没有其他线程来干扰的,如果有线程干扰,那就重新创建,直到创建成功。这样可以保证更新操作的原子性。
    • TLAB:
      • https://blog.csdn.net/xiaomingdetianxia/article/details/77688945
      • 全称是Thread Local Allocation Buffer,即线程本地分配缓存区
      • 为每一个线程预先在 Eden 区域分配一块内存,首先 TLAB 分配,对象的需要的内存大于了 TLAB 提供的,再采用 CAS 进行内存分配。

2.3、初始化零值

  • 当内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)

  • 保证了对象的实例字段在 Java 代码中可以不赋初值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

2.4、设置对象头

  • 初始化完成后,需要对象头去辨认我们这个新创建对象的一些信息。

  • 对象头来存储对象是哪个类的实例***、类的元数据信息*、对象的哈希码、对象的 GC 分代年龄等信息。

2.5、执行 init 方法

JVM会调用对象的构造函数去初始化值,执行方法

3、对象的访问定位有哪两种方式?

Java 程序通过栈上的 引用来操作堆上的具体对象。目前主流的访问方式有①使用句柄和②直接指针两种

3.1、句柄访问

如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

3.2、直接指针访问方式

引用中直接存的就是 Java 堆中的对象实例数据,然后堆中空间里有指向对象类型数据的地址。

3.3、对比

使用句柄的好处是引用中存储的是稳定的句柄地址,在对象被移动时指挥改变句柄中的实例数据指针,而 引用本身不需要修改。使用直接指针访问方式最大的好处就是速度快,节省了一次指针定位的时间开销。Hotspot 默认的时直接指针。

4、JVM内存分配和回收

  • JVM 内存分配是在堆内存上面进行分配,堆内存主要分为以下几个部分:新生代和老年代(元空间属于非堆);

  • 轻 GC:当我的Eden的内存了,会触发一次轻 GC, 将能够活下来的对象给幸存区,此时伊甸园区内存就清空了,继续运行,

  • 重 GC:直到新生区(伊甸园+幸存区)的内存满了就进行重 GC,将新生区清空,进入老年区

  • 如果新生和养老区都满了,就 OOM (内存溢出)了。

  • Full GC/Major GC(发生在老年代的 GC) 会清理新生区和老年区:

    • System.gc
    • 伊甸园区进入存活区放不下,去老年区也放不下
    • gc 线程与用户线程同时执行,那么用户线程依旧可能同时产生垃圾,如果垃圾较多无法放入预留空间
    • 新生代的晋升平均大小是大于老年代的剩余空间

5、堆内存中对象的分配的基本策略

5.1、对象优先在eden 区分配

大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC(发生新生代的的垃圾收集动作)。

5.2、大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组) 如果对象比较大,那么复制算法效率就比较低下。

5.3、长期存活的对象将进入老年代

  • 大部分的情况,对象都会现在 Eden 区域分配,然后进行了一次新生代垃圾回收,如果对象还活着,就进入进入幸存区 0 或 1,并且年龄还会+1,当它的年龄增到一定程度(默认15 岁),就会到老年代。对象晋升到老年代的年龄阈值:可以通过 -XX:MaxTenuringThreshold=某值来设置。
  • 动态对象年龄判断,如果幸存区中相同年龄的对象所有大小之和超过了幸存区的空间一半,那么大于等于该年龄的对象直接进入老年区,无需等到默认年龄。

13.GC如何判断对象可以被回收

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

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

GC Roots的对象有:

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

可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”

每个对象只能触发一次finalize()方法

由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。

6、如何判断对象是否死亡?

6.1、引用计数法

为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;

6.2、可达性分析算法

  • 主流的虚拟机都是采用 GC Roots Trancing 算法

  • 该算法的核心是从 GC Roots 对象作为起始点,利用数学中图论知识,途中可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存。

  • GC Roots 的对象(三中其 1):

    • 虚拟机的栈帧的局部变量表所引用的对象;
    • 本地方法栈的 JNI 所引用的对象;
    • 方法区的静态变量和常量所引用的对象;

7、强引用,软引用,弱引用,虚引用

1、强引用

  • 如果一个对象具有强引用,对于我们来说是不能缺少的对象垃圾回收器绝不会回收它。

  • 当内存空间不足,JVM 宁可抛出异常也不会回收它。

2、 软引用

软引用的对象是可有可无的对象。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

3、 弱引用

弱引用的对象也是可有可无的对象。垃圾回收器在扫描他所管辖的内存区域,一旦发现弱引用的对象,不管内存是否足够,都会回收它的内存。

4、虚引用

虚引用并不会决定对象的生命周期。他跟没有任何引用一样,在任何时候都可能被垃圾回收。

5、总结

  • 强引用:String str = "abc";lsit.add(str);
  • 软引用:如果弱引用对象回收之后,内存还是报警,继续回收软引用对象
  • 弱引用:如果虚引用对象回收之后,内存还是报警,继续回收弱引用对象
  • 虚引用:虚拟机的内存不够用,开始报警,这时候垃圾回收机制开始执行System.gc;String s ="abc"如果没有对象回收了,就回收没虚引用的对象

8、如何判断一个常量是废弃常量

  • 运行时常量池主要回收的是废弃的常量。
  • 如果当前没有任何 对象引用该常量的话,就说明常量就是废弃常量,

9、如何判断一个类是无用的类

  • 方法区主要回收的是无用的类

  • 如果要判断是无用的类要同时满足以下三个条件:

    • 该类的所有实例都被回收,也就是说 Java 堆中没有该类的任何对象
    • 加载该类的 ClassLoader 已经被回收
    • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

10、垃圾收集有那些算法,各自的特点

  • 标记-清除算法:标记无用对象,然后进行清除回收。

    • 优点:实现简单,不需要对象进行移动。
    • 缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率
  • 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

    • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
    • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
  • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

    • 优点:解决了标记-清理算法存在的内存碎片问题。
    • 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
  • 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

11、为什么要分新生区和老年区

为了提升 GC 效率,因为不同区使用不同的 GC 算法。

12、常见的垃圾收集器有那些?

收集器 串行、并行、并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记-整理 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用
G1 并发 both 标记-整理+复制算法 响应速度优先 面向服务端应用、将来替换CMS

13、JVM 调优

  • JVM 调优的目标是:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。重要指标:

    • 内存占用:程序正常运用需要的内存大小
    • 延迟:由于垃圾收集而引起的程序停顿时间
    • 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值
  • JVM 配置方面,可以先使用默认配合,堆初始是内存的 1/16,最大堆内存是内存的 1/4。

  • 调优的工具

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

    • -Xms 和-Xmx 的值设置一样,为了避免内存的动态调整,节约资源。
    • 新生代尽量设置大一些,让对象在新生代多存活一段时间,每次 Minor GC 尽可能多收集垃圾对象,防止进入老年代,进行 Full GC.
    • 老年代如果使用 CMS 收集器,新生代可以不用太大,因为 CMS 的并发收集速度也很快, 收集过程可以与用户线程并发执行。
  • 避免不需要的对象进入老年代

    • 避免创建过大的对象或者数组:过大的对象和数组在新生代没有足够的空间会进入,老年代,会提前出发 FullGC
    • 避免同时加载大量数据:从数据库或者 Excel 取大量数据,尽量分批读取
    • 当程序中有对象引用,如果使用完后,尽量设置为 null,比如 obj1=null。避免这些对象进入老年代
    • 避免长时间等待外部资源,缩小对象的生命周期,避免进入老年代。

其他

1、get和post的区别

get post
大小 (浏览器url的长度) post请求无限制
缓存 get请求浏览器会自动存储 post不会
历史 get请求记录会被存储在历史记录中, post不会
编码 get请求只能进行url编码 post请求支持多种
参数 get请求只接收ASCII字符 post无限制
安全 get请求参数暴露在url中 post放在Request body中,更加安全
回退 请求在浏览器反复的 回退/前进 操作是无害的, post 操作会再次提交表单请求。
TCP数据包: get请求发送一次,因为http header和 data是一并发送的。服务器响应200 post请求发送两次。先发送header,响应100 continue,再发送data,服务器响应 200。

2、http头部

HTTP有两类报文:

  • 请求报文:从客户向服务器发送的请求报文。
  • 响应报文:从服务器到客户的回答。

HTTP 请求头 一个 HTTP 请求头由请求行(request line)、请求头部(header)2 个部分组成,下图给出了请求报文的一般格式。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rD1tnzh7-1637023656154)(总结最终版.assets/f6543f47b815c963e3115da1b45113c0.png)] 我们从一个常见的 HTTP 请求开始吧,比如我们访问 leetcode-cn.com 的时候,浏览器发出的 HTTP 请求头部是:

Host: leetcode-cn.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3835.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br

这一切在你输入完网址并按下 Enter 之后浏览器会替你发送给 leetcode-cn.com的服务器,我们分别来看看这些字段代表了什么。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MaGpkwU4-1637023656156)(总结最终版.assets/dc0b2d8167e041cb5c7dbd800526444c.png)]

*HTTP 返回头*

HTTP 返回头也由两个部分组成,分别是: 状态行、消息报头,如下图: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IK5iJwy6-1637023656158)(总结最终版.assets/fe9e26bc28a07080b7c53493663f16db.png)] 在发出了之前的请求报文之后,leetcode-cn.com 的服务器就会进行返回,其中部分 HTTP 头部信息可能如下:

HTTP/2.0 200 OK
date: Sat, 12 Oct 2019 12:47:09 GMT
content-type: text/html; charset=utf-8
server: nginx/1.15.8
content-encoding: br

对于其中的字段来说,可以整理为以下表格:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MCiSzPya-1637023656160)(总结最终版.assets/b4bb7aa0756ebb028b66efe0f77342dc.png)]

另外,第一行中的 200 OK 也就是大家所熟悉的 HTTP 状态码了,常见的状态码分类有:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NAURfsog-1637023656162)(总结最终版.assets/50997935de8634c7fe8dd5e93f9e8e02.png)]

其中,面试时常被问到的特定状态码有:

  • 404 Not Found:服务器无法根据客户端的请求找到资源(网页)。
  • 304 Not Modified:未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。
  • 502 Bad Gateway:作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应,一般来说是后端服务器挂掉时前端网页服务器进行的返回。
  • 500 Internal Server Error:服务器内部错误,无法完成请求。

| Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| CMS | 并发 | 老年代 | 标记-整理 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
| G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用、将来替换CMS |

你可能感兴趣的:(面试总结,java)