Java面试题

Java基础面试题

Java基础

Java基础面试题

1.final和static的区别

都可以修饰类、方法、成员变量。
都不能用于修饰构造方法。
static 可以修饰类的代码块,final 不可以。
static 不可以修饰方法内的局部变量,final 可以。


static:

static 修饰表示静态或全局,被修饰的属性和方法属于类,可以用类名.静态属性 / 方法名 访问
static 修饰的代码块表示静态代码块,当 Java 虚拟机(JVM)加载类时,就会执行该代码块,只会被执行一次
static 修饰的属性,也就是类变量,是在类加载时被创建并进行初始化,只会被创建一次
static 修饰的变量可以重新赋值
static 方法中不能用 this 和 super 关键字
static 方法必须被实现,而不能是抽象的abstract
static 方法不能被重写

final:

final 修饰表示常量、一旦创建不可改变
final 标记的成员变量必须在声明的同时赋值,或在该类的构造方法中赋值,不可以重新赋值
final 方法不能被子类重写
final 类不能被继承,没有子类,final 类中的方法默认是 final 的

final可以用来修饰类,方法,属性,不能被继承

static可以修饰成员变量和成员方法,称为静态变量和静态方法,可以直接通过类名来进行访问,不用每次都new实例化,因为在编译后就已经分配好了内存。static是同一个类所有对象共享。
final可以修饰类,方法和变量,
final修饰的类,表示该类无法被任何其他类继承,
final修饰类中的方法,表示不能被重写,
final修饰的变量,表示该变量一旦被初始化就不能再修改了。

String,StringBuilder和StringBuffer的区别?

1、String类型的字符串对象是不可变的,一旦String对象创建后,包含在这个对象中的字符系列是不可以改变的,直到这个对象被销毁。

2、StringBuilder和StringBuffer类型的字符串是可变的,不同的是StringBuffer类型的是线程安全的,而StringBuilder不是线程安全的

3、如果是多线程环境下涉及到共享变量的插入和删除操作,StringBuffer则是首选。如果是非多线程操作并且有大量的字符串拼接,插入,删除操作则StringBuilder是首选。
在这里插入图片描述

解释下什么是面向对象?面向对象和面向过程的区别?

面向对象的三大特征:封装、继承、多态。 面向过程的思想:完成每个需求时,都要分析做什么、怎么做,需要面对每一个具体的步骤和过程,这些步骤相互调用和协作,共同需求,面向过程思想中每一步都需要我们去参与实现和操作。
面向对象的思想:把客观的实体抽象为对象,认为程序是由一系列的对象构成,将我们从执行者变成了指挥者。通过封装、继承和多态简化问题,提升代码的复用性和可维护性。

面向对象的三大特性?分别解释下?

封装,就是把抽象出来的数据和对数据的操作封装在一起,数据被保护在内部,是属性的私有化,可以通过公有的方法访问私有属性,通过封装,可以对属性进行数据访问限制,也保证了程序的可维护性,封装不仅仅是属性,也可以是方法。
继承,子类继承父类中的属性和行为,并能扩展新的能力,只支持单继承。
多态,同一个行为具有不同的表现形式和能力,方法的重写,重载和动态连接构成了方法的多态。
对象的多态前提是两个对象存在继承关系,本质是父类的引用指向了子类的对象,编译类型看左边,运行类型看右边,运行类型是可以变化的,编译类型在定义对象时,就确定了,不能改变。

JDK、JRE、JVM 三者之间的关系?

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,JVM在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
JRE(Java Runtime Environment)是 Java 运行时环境。包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:
Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。
JDK是Java 程序的开发工具,包含范围递增。
在这里插入图片描述

重载和重写的区别

方法的重载和重写都是实现多态的形式,
不同的是,前者实现的是编译时的多态性,后者实现的是运行时的多态性
重载是一系列参数不同名字相同的方法,发生在一个类中,可以修改返回值。
重写是子类继承父类后重新实现父类的方法,方法名和参数列表必须相同,不能修改返回值。
在这里插入图片描述

Java中是否可以覆盖【就是重写】(override)一个private或者是static的方法?

Java中的static方法不能被覆盖,因为方法覆盖是运行时动态绑定的,static方法是编译时静态绑定的,static方法跟类的任何实例都不相关。
Java中也不可以覆盖private,1.重写是子类中的方法和子类继承的父类中的方法一样(函数名,参数,参数类型,反回值类型),但是子类中的访问权限要不低于父类中的访问权限。重写的前提是必须要继承,private修饰不支持继承,因此被私有的方法不可以被重写。
2.即被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。

String a = “ab”; String b = “a” + “b”; a == b 是否相等

a和b都是常量字符串,b这个变量,在编译时不会出现不可控的因素,直接被赋予为ab,a初始化时,会在字符串常量池中创建一个字符串ab,并返回字符串常量池的引用,对于变量b,赋值ab时,首先在字符串常量池中查找是否含有相同的字符串。如果存在,则直接返回该字符串的引用

String str = new String(“Hello World”) + new String(“!”);

String str1=str.intern();  
System.out.print(str == str1);

先创建一个StringBuilder对象,然后调用append()方法进行拼接,最后用toString()方法得到一个常量

描述一下值传递和引用传递的区别

值传递:

方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值。

引用传递:

也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址;

在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象。

String a = “ab”; String b = “a” + “b”; a == b 是否相等

a和b都是常量字符串,b这个变量,在编译时不会出现不可控的因素,直接被赋予为ab,a初始化时,会在字符串常量池中创建一个字符串ab,并返回字符串常量池的引用,对于变量b,赋值ab时,首先在字符串常量池中查找是否含有相同的字符串。如果存在,则直接返回该字符串的引用

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

不属于,是final修饰的Java类。

java中的基本数据类型:byte、char、short、int、long、float、double、boolean

包装类 Integer

在这里插入图片描述
在这里插入图片描述

String str="i"与 String str=new String(“i”)一样吗?

不一样。他们不是同一个对象

前者如果定义多个变量都为相同值的话,会共用同一个地址,创建的对象应该放在了常量池中;

后者是创建了一个新的对象,放在的是堆内存中。

如何将字符串反转?

使用StringBuffer 或 StringBuilder 的 reverse 成员方法。

java 中 IO 流分为几种?

字节流:InputStream、OutputStream

字符流:Reader、Writer

字节流是最基本的

1.字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;

2.字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。

读文本的时候用字符流,例如txt文件。读非文本文件的时候用字节流,例如mp3。

BIO、NIO、AIO 有什么区别?

BIO:Block IO 同步阻塞式 IO

NIO:Non IO 同步非阻塞 IO

AIO:Asynchronous IO 异步非阻塞IO

BIO是一个连接一个线程。JDK4之前的唯一选择
NIO是一个请求一个线程。JDK4之后开始支持,常见聊天服务器
AIO是一个有效请求一个线程。JDK7之后开始支持,常见相册服务器

Linux常见命令

查看文件目录pwd
切换目录cd
创建文件夹mkdir
删除文件夹rm
移动或者重命名mv
查看文件内容cat 或者more
查看网卡信息ipconfig
杀进程kill

抽象类

当父类的一些方法不能确定时,可以用abstract关键字来修饰该方法,这个方法
就是抽象方法,用abstract来修饰该类就是抽象类。
在这里插入图片描述

多态

在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

动态绑定机制

在这里插入图片描述

String str = new String(“Hello World”) + new String(“!”);

String str1=str.intern();  
System.out.print(str == str1);

先创建一个StringBuilder对象,然后调用append()方法进行拼接,最后用toString()方法得到一个常量

索引结构为什么会采用B+树?而不是二叉树,AVL树,B树?

B+树相对于二叉树来说,层级更少,搜索效率高;
对于B树,无论是叶子结点还是非叶子结点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
B+树只需要遍历叶子结点就可以实现整棵树的遍历,而其他的树形结构,要中序遍历才能访问所有的数据。

接口和抽象类的区别

相同点:都不能被实例化,接口的实现类和抽象类的子类只有全部实现了接口或者抽象类中的方法后才可以被实例化。
不同点:1.接口只能定义抽象方法不能实现方法,抽象类既可以定义抽象方法,也可以实现方法

接口和抽象类是面向对象编程的重要组成部分,它们有一些相似之处,例如都不能被实例化,都可以包含抽象方法,但也有很多区别,具体如下:

概念不同。接口是对动作的抽象,抽象类是对根源的抽象。
功能不同。抽象类表示的是“这个对象是什么”,接口表示的是“这个对象能做什么”。
其他成员不同。接口中只能包含抽象方法,不能定义静态方法;抽象类可以包含普通方法,也可以定义静态常量属性。
继承机制不同。一个类可以实现多个接口,但只能继承一个抽象类。

接口里只能包含抽象方法的默认方法,不能为普通方法提供方法实现;抽象则完全可以包含普通方法。

接口里不能定义静态方法;抽象类可以定义静态方法。

接口里只能定义静态常量,不能定义普通成员变量;抽象类里既可以定义普通成员变量,也可以定义静态常量

接口里不包含构造器;抽象类里可以包含构造器,抽象类可以包含构造器,它的作用是让子类调用这些构造器完成抽象类的初始化操作

接口里不能包含初始化块;但抽象类完全可以包含初始化块

一个类最多只能有一个直接父类,包括抽象类;但一个类可以实现多个接口,通过实现多个接口弥补Java单继承的不足

概述Java的异常处理体系。并罗列出常见的5种运行时异常。井说明产生的情况Java的异常处理体系包括以下几个主要部分:

异常类型:在Java中,异常类型分为两大类:检查型异常(Checked Exceptions)和非检查型异常(Unchecked Exceptions)。检查型异常在编译时期就能被检测到,例如尝试打开一个不存在的文件。非检查型异常在运行时期才能被检测到,例如除以零。
抛出异常:在Java中,可以使用 throw 关键字来抛出异常。这通常在发现某种错误条件的情况下发生,例如,当尝试打开一个不存在的文件时,可以抛出一个 FileNotFoundException。
捕获异常:在Java中,可以使用 try/catch 块来捕获和处理异常。如果在 try 块中的代码抛出了异常,那么控制权就会立即转移到相应的 catch 块,然后在 catch 块中处理这个异常。
** finally** 块:无论是否发生异常,Java都会执行 finally 块中的代码。这常用于资源的清理工作,例如关闭文件、释放锁等。
异常链:当一个方法抛出一个异常时,这个异常可以在被调用栈中"冒泡"直到它被处理或者直到程序崩溃。这个过程也被称为"异常链"。

以下是常见的五种运行时异常(非检查型异常)以及它们产生的情况:

NullPointerException:当试图访问或修改一个空对象引用(null)的属性或调用其方法时,会发生此异常。例如,尝试调用一个未初始化的对象的方法。
IndexOutOfBoundsException:当试图访问数组、字符串或其他集合类型的元素,但是索引超出了其实际长度时,会发生此异常。例如,尝试访问一个长度为5的数组的第6个元素。
ArithmeticException:当执行非法数学操作,如除以零或者操作结果超过了可以表示的最大值时,会发生此异常。例如,尝试执行一个除以零的操作。
ConcurrentModificationException:当并发修改集合(如ArrayList、LinkedList等)时,会抛出此异常。例如,在一个线程中遍历一个ArrayList的同时,另一个线程尝试修改这个ArrayList。
RuntimeException:这是一个"父"异常,指的是所有非检查型异常的根。通常情况下,我们不会直接抛出这个异常,而是抛出它的子类。例如,所有这些子类都继承自RuntimeException:NullPointerException、IndexOutOfBoundsException、ArithmeticException等。

java多态了解吗?简单介绍下它的实现原理和应用场景?

请概述如何在应用中排除死锁问题

在应用中排除死锁问题可以采取以下几种方法:

合理分配资源。一次性合理地分配所有的资源,只要有一个资源得不到分配,也不给这个进程分配其他资源。
允许抢占资源。发现系统中有进程死锁时,可以强制性地剥夺抢占某些进程的资源,然后分配给死锁进程,以解除死锁状态。
撤销进程挂起。不用销毁这部分的死锁进程,可以将其挂起到CPU外,将资源空出来让给死锁进程。

new String和字面量创建区别和相同点

内存分配方式不同:使用字面量创建字符串时,Java会在常量池中查找是否具有相同的字符串,如果存在,则直接返回该字符串的引用,如果不存在,则创建新的字符串对象并把它存入常量池中。而使用new String()字符串时,Java会在堆中为该字符串分配新的内存空间。
字符串对象的可变性不同:使用字面量创建的字符串对象是不可变的,即一旦创建,其值就无法改变,而使用new String()创建字符串,对象是可变的,可以通过调用字符串的一些方法来修改其值。

String“+”的内部实现

先创建一个StringBuilder对象,然后调用append()方法进行拼接,最后用toString()方法得到一个常量

final关键字能加在抽象类上吗?为什么?

没有意义,抽象类方法是要子类继承实现内部方法的,被final修饰的类不能再被继承和修改。

你对Java里的常量池是如何理解的?

Java中的常量池,实际上分为两种型态:静态常量池和运行时常量池。
静态常量池,就是.class文件中的常量池,class文件中的常量池不仅仅包含字符串字面量,还包含类,方法的信息,占用class文件绝大部分空间,
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

==比较的是什么

当比较的是基础类型时,如果值相等,则返回true否则返回false
比较的是引用类型时,比较的是引用类型的地址

equals比较的是什么

equals是Object类,只能判断引用类型,比较的是两个对象的hashcode是否相同,所以默认判断内存地址是否相同,但在Object的子类中可以重写equals方法,用于判断内容是否相等。如Integer,String。

Integer和int的各种==判断

1.首先先看一下Integer和Integer的对比,==判断对象地址是否相同,equals判断值是否相同。Integer重写了equals方法,用于比较Integer对象的value值是否相同,如果没有重写equals方法的话,equals比较的也是对象地址值是否相同。
如果对比的是两个integer对象都不是new出的,且值范围在[-128~127]之间,根据Integer的缓存机制,两个对象先装箱指向同一个缓存对象,= =和equals都会返回true,如果范围不在[-128~127]之间,则会直接new Integer(),创建一个新对象,= =会返回false,而equals方法会返回true。
2.Integer和int对比,和两个Int判断是一样的,因为Integer会有一个拆箱的过程,只要这两个值一样。不管是= =还是equals都是相等的

数组的三种创建方式

1.使用new关键字创建数组
数据类型[] 数组名 = new 数据类型{数组长度}
2.使用大括号初始化数组
数据类型[] 数组名 = {元素}
3.使用静态初始化数组
数据类型[] 数组名 = new 数据类型{元素}

反射

反射(Reflection),是指Java程序具有在运行期分析类以及修改其本身状态或行为的能力。 通俗点说 就是 通过反射我们可以动态地获取一个类的所有属性和方法,还可以操作这些方法和属性。

反射机制

实例的创建

一般我们创建一个对象实例Person zhang = new Person(); 虽然是简简单单一句,但JVM内部的实现过程是复杂的:
1.将硬盘上指定位置的Person.class文件加载进内存
2.执行main方法时,在栈内存中开辟了main方法的空间(压栈-进栈),然后在main方法的栈区分配了一个变量zhang。
3.执行new,在堆内存中开辟一个 实体类的 空间,分配了一个内存首地址值
4.调用该实体类对应的构造函数,进行初始化(如果没有构造函数,Java会补上一个默认构造函数)。
5.将实体类的 首地址赋值给zhang,变量zhang就引用了该实体。(指向了该对象)
在这里插入图片描述

反射创建对象的方法

1.调用class对象的newInstance()方法

Class c1 = Class.forName("com.zj.demotest.domain.Person");
Person p1 = (Person) c1.newInstance();
p1.eat();

注意:Person类必须有一个无参的构造器且类的构造器的访问权限不能是private

2.使用指定构造方法Constructor来创建对象
如果我们非得让Person类的无参构造器设为private呢,我们可以获取对应的Constructor来创建对象

Class c1 = Class.forName("com.zj.demotest.domain.Person");
Constructor<Person> con =  c1.getDeclaredConstructor();
con.setAccessible(true);//允许访问
Person p1 = con.newInstance();
p1.eat();

注意:setAccessible()方法能在运行时压制Java语言访问控制检查(Java language access control checks),从而能任意调用被私有化保护的方法、域和构造方法。 由此我们可以发现单例模式不再安全,反射可破之!

反射机制

有哪些方式获取类的 Class 对象

1.通过 类.class获取
Class< Reflect> class1 = Reflect.class;
2.通过 对象.getClass()获取
Class<? extends Reflect> class2 = new Reflect().getClass(); 3.通过 类Class.forName()获取 Class<?> class3 = Class.forName(“interview.Reflect”);
4.通过 类ClassLoader.loadClass()获取
Class<?> class4 = Reflect.class.getClassLoader().loadClass(“interview.Reflect”);

哪种方式获取 Class 对象效率最好

1.通过对象调用 getClass() 方法来获取

Person p1 = new Person();
Class c1 = p1.getClass();

像这种已经创建了对象的,再去进行反射的话,有点多此一举。 一般是用于传过来的是Object类型的对象,不知道具体是什么类,再用这种方式比较靠谱
2.类名.class

Class c2 = Person.class;

这种需要提前知道导入类的包,程序性能更高,比较常用,通过此方式获取 Class 对象**,Person类不会进行初始化 **
3.通过 Class 对象的 forName() 静态方法来获取,最常用的一种方式

Class c3 = Class.forName("com.zj.demotest.domain.Person");

这种只需传入类的全路径**,Class.forName会进行初始化initialization步骤,即静态初始化(会初始化类变量,静态代码块)。
4.通过 类ClassLoader.loadClass()获取

public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException {
        Person p1 = new Person();
        Class c1 = p1.getClass();
        Class c2 = Person.class;
        Class c3 = Class.forName("com.zj.demotest.domain.Person");

        //第4中方式,类加载器
        ClassLoader classLoader = TestReflection.class.getClassLoader();
        Class c4 = classLoader.loadClass("com.zj.demotest.domain.Person");


        System.out.println(c1.equals(c2));
        System.out.println(c2.equals(c3));
        System.out.println(c3.equals(c4));
        System.out.println(c1.equals(c4));
    }
}

loadClass的源码:

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

loadClass 传入的第二个参数是"false",因此它不会对类进行连接这一步骤,根据类的生命周期我们知道,如果一个类没有进行验证和准备的话,是无法进行初始化过程的,即不会进行类初始化,静态代码块和静态对象也不会得到执行
因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例,一个类在 JVM 中只会有一个 Class 实例

new一个对象的过程是怎样的?

第一步类加载和初始化(第一次使用该类),第二步创建对象
加载,验证,准备,解析,初始化。
加载:在 加载阶段,类加载器会将类对应的.class文件中的二进制字节流读入到内存中,将这个字节流转化为方法区的运行时数据结构,然后在堆区创建一个 java.lang.Class 对象(类相关的信息),作为对方法区中这些数据的访问入口
验证:验证类是否符合Java规范和JVM规范
准备:为类的静态变量分配内存,初始化为系统的初始值
解析:将符号引用转为直接引用的过程
初始化:为类的静态变量赋予正确的初始值
1、在堆区分配对象需要的内存
2、对所有实例变量赋默认值
3、执行实例初始化代码
4. 将堆区对象的地址赋值给栈区的引用变量

Mysql

能否举个数据库死锁的例子吗?顺便谈谈你认为应该如何避免死锁?

死锁产生的根本原因就是多个进程之间互相占用了对方的资源不释放,导致所有进程都无法继续推进下去的一种状态。
多个锁之间的嵌套产生死锁。
死锁一旦发生,我们就无法解决了。所以我们只能避免死锁的发生。
既然死锁需要满足四种条件,那我们就从条件下手,只要打破任意规则即可。

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。下面用java代码来模拟一下死锁的产生。

索引分类

在Inndb存储引擎中,根据索引的存储形式,可以分为两种
聚集索引:将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据,特点是必须有且只有一个
二级索引:将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键,可以存在多个
在这里插入图片描述

回表查询

先走二级索引找到对应的主键值,再根据主键值去聚集索引中拿到这一行的行数据。

覆盖索引

创建一个索引,该索引包含查询中用到的所有字段,称为“覆盖索引”。

使用覆盖索引,MySQL 只需要通过索引就可以查找和返回查询所需要的数据,而不必在使用索引处理数据之后再进行回表操作。

覆盖索引可以一次性完成查询工作,有效减少IO,提高查询效率。

InnDB引擎的底层数据结构是什么

数据库的日志有哪些

redo log(重做日志):用来实现事务的持久性和恢复能力的日志记录
undo log(回滚日志):用于记录数据被修改前的数据,作用有两个:MVCC和提供回滚

Mysql的ACID

事务的概念

保证一组数据库的操作,要么同时成功,要么同时失败

原子性:指事务是一个不可分割的整体,类似于一个不可分割的原子
隔离性:多个事务之间要相互隔离,互不干扰
一致性:保障事务前后这组数据的状态是一致的,要么同时成功,要么同时失败
持久性:指事务一旦被提交,这组操作修改的数据就真的发生变化了。即便接下来数据库故障也不应该对其有影响。
在这里插入图片描述

Mysql存储引擎

InnoDB 和 MyISAM

InnoDB 和 MyISAM区别

Inndb是聚簇索引,在叶子节点存放的是整条数据
MyISAM:是非聚簇索引,叶子节点存放的是主键id。

Inndb支持事务,也支持行锁。支持外键约束,从而保证数据的完整性和正确性。
MyISAM不支持事务,不支持外键。支持表锁,不支持行锁。访问速度快。

隔离级别有哪几种?分别解决了什么问题?

READ_UNCOMMITTED(读未提交):是最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读,不可重复度,幻读。
READ_COMMITTED(读已提交):一个事务提交之后,它做的变更才会被其他事务看见,可以阻止脏读,但不能阻止不可重复读和幻读。
REPEATABLE_READ(可重复读):在一个事务当中执行两次相同的sql结果一样,可以阻止脏读和不可重复读,但幻读会时而发生。
SERIALIZABLE(串行化):最高的隔离级别,InnDB隐式的将全部的查询语句加上了共享锁,解决了脏读,幻读和不可重复读的问题,但这将严重影响程序的性能。

不可重复读:同样的sql在一个事务当中查询出来的数据不一致
可重复读:在一个事务当中执行两次相同的sql结果一样

幻读和不可重复读的区别是什么?

不可重复读的重点是修改
幻读的重点是新增和删除

Mysql解决幻读了吗?为什么?

解决了,快照读解决幻读是因为Mysql在可重复读的隔离级别下,是通过MVCC机制避免幻读的,在数据启动时对数据库拍了个快照,它保留了那个时刻数据库的数据状态,那么这个事务后续的读取都可以从这个快照中获取,哪怕其他事务新加了数据,也不会影响到快照中的数据,也就不会出现幻读了。
当前读是如何避免幻读的呢,当前读需要获取最新状态的数据,在当前读时,需要对这段区间都加上锁,让别的事务阻塞,无法插入,因此,InnDB引擎为了解决可重复读隔离级别使用当前读而造成的幻读问题,引入了间隙锁。
总结一下,针对快照读,也就是普通的SELECT语句,是通过MVCC解决了幻读问题。针对当前读,也就是select…for update语句,是通过next-key lock(记录锁+幻读锁)方式解决了幻读。
读写冲突可以用MVCC解决,写写冲突可以用锁机制解决。

在这里插入图片描述在这里插入图片描述

Mysql中,Repeatable Read(rr,可重读隔离级别)是如何解决不可重复读呢,讲讲你的理解?

【答案】
通过MVCC(多版本并发控制),它实现了Mysql在InnDB引擎下RC级别与RR级别下对数据库的并发访问,每次select操作时会访问数据库中的版本链记录,其他事务可以修改此版本链记录,而select根据当前隔离级别去版本链中找到对应的版本记录,实现了读写的并发执行。在RR级别下,只有在第一次快照读时生成ReadView,后面会延续使用,也就是有了新版本了我也不读取,相当于掩耳盗铃,解决了不可重复读的问题。

悲观锁和乐观锁是如何实现的,各自有哪些使用场景?请举例说明?

【答案】
悲观锁获取到锁时,必须要锁住资源,因为它会认为别的线程会来争抢数据,造成数据结果错误,所以悲观锁会在每次获取并修改数据时,都把数据锁住,让其他线程无法访问该数据。比如A拿到锁,正在操作资源,B只能进行等待,直到A线程执行完后释放锁,CPU才会唤醒等待中的B线程。乐观锁在操作资源时不会认为会有别的线程干扰。乐观锁为了保障数据的正确性,它在操作之前,会先判断自己操作期间,有没有其他线程操作,如果没有,直接操作,如果有,根据业务选择重试或报错。典型的select for update语句,就是悲观锁,在提交之前,不允许第三方来操作该数据,高并发环境吃不消。乐观锁有利用version字段实现乐观锁,version代表这条数据库的版本,操作数据时不需要获得锁,操作完准备更新时,对比版本号和获取数据时是否一致。

Mysql事务怎么实现

详细请看:事务实现
首先我们要先知道事务是什么,事务是一种确保数据库操作完整性和一致性的机制,具有ACID四大特性。
事务的原子性,一致性,持久性是通过redo log日志和undo log日志实现的,隔离性是通过锁和MVCC机制实现的。
redo log是重做日志,是用来实现事务的持久性和恢复能力的日志记录。
undo log是回滚日志,用于记录数据被修改前的数据,作用有两个:提供回滚和MVCC。

Mysql数据库中锁的分类,请详细说一下

详细请看:数据库锁的分类
数据库按锁的粒度划分,可分为行级锁,表级锁和页级锁
行级锁是一种排他锁,防止其他事务修改此行。特点是:开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度也最高。行级锁分为共享锁和排他锁。
表级锁是
根据类型划分,可以分为共享锁,排他锁,意向共享锁和意向排他锁

数据库group by 在mysql和sqlserver中的区别

在Sqlserver中,如果在select语句中使用了group by,那么查询的字段只能是用来分组的字段,或者是对聚合函数包裹着的字段,不然会报错,这是因为同一组数据其他字段不同的话,sqlserver不知道如何做处理,不知道该保留那一条数据的值,

能简单说一下你对Mysql日志的了解吗

Mysql日志系统是数据库的重要组件,用于记录数据库的更新和修改。若数据库发生故障,可通过不同日志记录恢复数据库的原来数据。Mysql的日志有很多种,如binlog,错误日志等,Inndb存储引擎还提供了两种日志,如redo log(重做日志)和undo log(回滚日志)

Mysql日志相关

此处了解即可,mysql内部日志主要区分为:
事务日志:
工作模式:基于InnoDB存储引擎的MySQL之所以可以从崩溃中恢复,正是依赖于事务日志,当数据库实例宕机后,重启时MySQL会自行检查事务日志,然后依次处理;
事务日志分为redo log和undo log两种:
(1)、对于事务日志中未正常提交的事务,则会记录到undo log中,因为事务未正确执行完,因此必须回滚,从而保证数据一致性
(2)、对于事务日志中已正常提交但未同步到持久化存储上时,则会记录到redo log中,因此MySQL会重新执行一遍事务,然后让数据存储到磁盘上,从而保证数据一致性

二进制日志
MySQL中的二进制日志(binary log)是一个二进制文件,主要用于记录可能引起数据库内容更改的SQL语句或数据行记录,例如新增(Insert)、更新(Update)、删除(Delete)、授权信息变更(Grant Change)等,除记录这些外,还会记录变更语句的发生时间、执行时长、操作数据等额外信息,但是它不会记录诸如Select、Show等这些不会引起数据修改的SQL语句。
binary log 主要用于主从复制等集群模式

慢日志与错误日志
(errlog)错误日志,作用:Mysql本身启动,停止,运行期间发生的错误信息
(slow query log)慢查询日志,  作用:记录执行时间过长的sql,时间阈值可以配置,只记录执行成功

undo log的作用是什么,能简单说说吗

undo log是回滚日志,用于记录数据被修改前的数据。主要有两个作用:1.提供回滚,2.实现MVCC。当事务对数据库进行修改,InnDB引擎不仅会记录redo log,还会记录undo log。如果事务执行失败,或调用了rollback,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改前的样子。但是undo log与redo log不一样,它是逻辑日志。它对sql语句执行相关的进行记录。当发生回滚时,InnDB引擎会根据undo log日志中的记录做与之前相反的工作。

redo log是什么?它和binlog的区别是什么

redo log是重做日志,是InnDB引擎层的日志,用来记录事务操作引起数据的变动,记录的是数据页的物理修改。InnDB引擎对数据的更新,是先将更新记录写入redo log日志中,然后会在系统空闲的时候或者是按照设定的更新策略再将日志中的内容更新到磁盘当中,这就是预写式技术(WAL),这种技术可以大大减少IO操作的频率,提升数据刷新的效率。
redo log是InnDB引擎特有的,binlog是Mysql的Server实现的,所有引擎均可以使用;
redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是数据的原始逻辑;
redo log是循环写的,空间固定会用完,binlog是可以追加写入的,文件写到一定大小后会切换到下一个,并不会覆盖之前的日志。

binlog记录了什么?有什么用处

binlog是归档日志,是服务层的日志,binlog主要记录数据库的变化情况,内容包括数据库所有的更新操作,所有引起数据库变动的,都要记录进binlog中

redo log的底层设计是什么样的

客户端在进行事务操作时,会发起请求去操作mysql服务器,在mysql服务器的InnDB引擎当中,有内存结构和磁盘结构,磁盘结构中放了很多的数据文件,在内存结构中有Buffer Pool(缓冲池),在缓冲池中,缓冲了许多数据页信息,当进行update时,首先要经过缓冲区,在缓冲区中要去查找有没有所要更新的这一块的数据,如果没有,它就会通过后台线程,把我们的数据从磁盘中读取出来,然后再缓存到缓冲区当中,接下来就可以直接在缓冲区进行操作数据,缓冲区的数据就发生了变更,但磁盘没有发生变更,这时这个数据页我们称为脏页,现在要在一定的时机通过后台线程要刷线到磁盘当中,此时缓冲区当中的数据就和磁盘当中的数据保持一致,但是脏页的数据并不是实时刷新的,当脏页在往磁盘当中刷新的时候出错了,内存当中的数据没有刷新到磁盘当中,此时持久性就没有得到保障。redo log出现之后,当我们对缓冲区的数据进行增删改之后,它首先会把增删改的数据记录到redolog Buffer当中,在redolog Buffer当中记录数据页的物理变化,当事务提交时,他会把redolog Buffer,也就是重做日志缓冲区当中的数据页变化更新到磁盘中,持久化的保存在磁盘文件当中,在过一段时间之后,进行脏页刷新时出错了,会通过redolog来进行恢复。所以说redolog就是为了保证我们在进行脏页刷新发生错误时进行数据恢复,从而保证数据的持久性。

怎么保证redolog和binlog的一致性

redo log使用两阶段提交来保证redolog和Binlog的一致性,第一阶段是先做一个准备工作,开启一个事务提交器,让所有的资源准备好,然后会去收集所有的资源状态,当所有的资源的状态都准备好了之后,再进入第二阶段,发出一个commit的命令,所有的资源进行一个commit提交。
在这里插入图片描述

MySQL索引为什么要用b+树,为啥不用红黑树

B+树只有叶子节点用来存放数据,其余节点用来索引,红黑树多用于内部排序,即全部放在内存中。B+树只需要遍历叶子节点就可以实现整棵树的遍历,而其他的树形结构,要中序遍历才能访问所有的数据。

B+树相对于B树结构的优点

1.B+树只有叶子节点用来存放数据,其余节点用来索引,在数据量比较大的情况下,降低了树的深度;
2.由于数据只存在叶子节点,每次查询的路径深度相同,提高了查询稳定性
3.叶子节点有两个指针,分别指向相邻的节点,便于范围查询

数据库隔离级别,每个都解决了什么问题

索引在哪些情况下会失效?举例说明

1.如果索引了多列,也就是联合索引,需要遵循最左前缀法则,也就是查询从索引的最左列开始,并且不跳过索引中的列,如果跳跃某一列,索引将部分失效(后面的字段索引失效)
2.联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效
3.在查询条件中,对索引列使用函数或表达式,索引失效
4.在查询时使用了左模糊查询或左右模糊查询时,也就是like %XX或like %XX%
5.字符串类型字段使用时不加引号,索引失效

为什么like%放前面会失效

最左前缀原则,因为它使得MYSQL无法使用B树索引来快速定位符合条件的行,而需要扫描整个索引树和数据表。

怎么在MySQL中实现乐观锁

在MySQL中实现乐观锁可以使用版本号时间戳等机制来控制并发操作。下面介绍两种常见的实现方式:

使用版本号(Versioning):

在数据表中增加一个用于记录数据版本的字段,每次更新数据时,该字段的值都会递增当一个事务读取数据后,它可以将该数据版本的字段值一同读取,并在提交事务时,检查数据版本的字段值是否与最初读取的一致。如果一致,说明在此期间没有其他事务修改过该数据,可以执行更新操作;如果不一致,说明有其他事务修改过该数据,此时可以根据具体情况选择回滚事务或者重试操作。

例如,假设有一个订单表(orders),其中包含订单的基本信息和版本号(version),版本号的初始值为1。当一个事务执行更新操作时,可以按照以下步骤进行:

START TRANSACTION;
SELECT version, order_status FROM orders WHERE id = 1 FOR UPDATE;
-- 这里可能会进行一些业务处理
UPDATE orders SET order_status = 'shipped', version = version + 1 WHERE id = 1;
COMMIT;

在更新操作中,将订单状态更新为’shipped’,并将版本号递增1。如果在事务执行期间有其他事务修改了该订单的状态,那么版本号的值会发生变化,导致提交事务时版本号不一致,从而阻止了该事务的执行。

使用时间戳(Timestamp):

在数据表中增加一个用于记录数据最后修改时间的时间戳字段。当一个事务执行更新操作时,可以将其开始时间作为时间戳的值,与数据表中的时间戳进行比较。如果一致,说明在此期间没有其他事务修改过该数据,可以执行更新操作;如果不一致,说明有其他事务修改过该数据,此时可以根据具体情况选择回滚事务或者重试操作。

例如,假设有一个订单表(orders),其中包含订单的基本信息和最后修改时间戳(timestamp),时间戳的初始值为当前时间戳。当一个事务执行更新操作时,可以按照以下步骤进行:

START TRANSACTION;
SELECT timestamp, order_status FROM orders WHERE id = 1 FOR UPDATE;
-- 这里可能会进行一些业务处理
UPDATE orders SET order_status = 'shipped', timestamp = CURRENT_TIMESTAMP WHERE id = 1;
COMMIT;

在更新操作中,将订单状态更新为’shipped’,并将时间戳更新为当前时间戳。如果在事务执行期间有其他事务修改了该订单的状态,那么时间戳的值会发生变化,导致提交事务时时间戳不一致,从而阻止了该事务的执行。

需要注意的是,使用乐观锁实现并发控制时,需要在事务开始时读取数据版本或时间戳,并在提交事务时检查其一致性。如果在事务执行期间有其他事务修改了该数据,那么会导致读取的数据版本或时间戳与提交时的一致性检查失败,从而防止了并发操作的冲突。

如何优化慢查询语句(可以用在项目中)

通过索引优化:在表中添加合适的索引可以显著提升查询效率。
避免全表扫描:可以通过限制查询条件或者分页查询来提升查询效率。
优化查询语句:可以通过合适的JOIN或子查询或优化where条件来提升查询效率。
数据库表结构优化:可以通过拆分大表,归档历史数据等方式来优化表结构,提升查询效率。

能否举个数据库死锁的例子吗?顺便谈谈你认为应该如何避免死锁?

死锁产生的根本原因就是多个进程之间互相占用了对方的资源不释放,导致所有进程都无法继续推进下去的一种状态。
多个锁之间的嵌套产生死锁。
死锁一旦发生,我们就无法解决了。所以我们只能避免死锁的发生。
既然死锁需要满足四种条件,那我们就从条件下手,只要打破任意规则即可。

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。下面用java代码来模拟一下死锁的产生。

MySQL的可重复读隔离级别是否可以解决幻读问题?怎么解决的?

快照读解决幻读是因为Mysql在可重复读的隔离级别下,是通过MVCC机制避免幻读的,在数据启动时对数据库拍了个快照,它保留了那个时刻数据库的数据状态,那么这个事务后续的读取都可以从这个快照中获取,哪怕其他事务新加了数据,也不会影响到快照中的数据,也就不会出现幻读了。
当前读是如何避免幻读的呢,当前读需要获取最新状态的数据,在当前读时,需要对这段区间都加上锁,让别的事务阻塞,无法插入,因此,InnDB引擎为了解决可重复读隔离级别使用当前读而造成的幻读问题,引入了间隙锁。总结一下,针对快照读,也就是普通的SELECT语句,是通过MVCC解决了幻读问题。针对当前读,也就是select…for update语句,是通过next-key lock(记录锁+幻读锁)方式解决了幻读。

为什么innodb中使用自增主键

InnoDB 是 MySQL 数据库中的一种存储引擎,它使用自增主键有几个重要的原因:

性能优化:InnoDB 存储引擎将数据存储在聚簇索引中,主键是聚簇索引的键。使用自增主键可以使得数据在磁盘上的存储连续,从而减少了 I/O 操作次数,提高了查询性能。
避免主键冲突:自增主键可以自动生成唯一的标识符,避免了手动指定主键时可能出现的重复或冲突问题。
提高索引效率:自增主键的顺序性可以保证在查询时更快地定位到对应的数据记录,特别是对于范围查询和排序操作,因为自增主键是按照顺序生成的,所以可以更快地定位到数据范围。
方便管理:自增主键在插入数据时自动增长,不需要手动指定主键的值,简化了数据插入的过程。

需要注意的是,虽然使用自增主键有很多优点,但也有一些限制和考虑。例如,如果表中的数据量非常大,可能会出现主键耗尽的问题。此外,如果需要进行分布式部署或多台 MySQL 数据库之间进行数据同步,可能需要考虑其他的主键生成策略。

MVCC的具体概念

MVCC是多版本并发控制,读取数据时通过一种类似快照的方式将数据保存下来,维护一个数据的多个版本,使得读写操作没有冲突。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段,undo log日志,read view。

能说一下MVCC是如何实现的吗?

MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段,undo log,read view。

InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。

InnoDB 的MVCC是通过 read view 和undolog版本链实现的,版本链保存有历史版本记录,通过read view 判断当前版本的数据是否可见,如果不可见,再从版本链中找到上一个版本,继续进行判断,直到找到一个可见的版本。

MVCC实现原理

MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段,undo log,read view。

隐式字段

每行记录除了我们自定义的字段外,还有三个隐式字段。
DB_ROW_ID 隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个row_id
db_trx_id:用来存储每次对某条记录进行修改时的事务id
db_roll_pointer:回滚指针,用于配合undo log,指向这条记录的上一个版本。

undo log日志

回滚日志,在insert,update,delete的时候产生的便于数据回滚的日志。
在insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
而update,delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

undo log版本链

不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧纪录,链表尾部是最早的旧纪录。

readview

ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
ReadView最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个ReadView的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取到的是当前行记录的undolog中某个版本的数据。
然后再根据版本链的数据访问规则来
不同的隔离级别,生成ReadView的时机不同:
RC:在事务中每一次执行快照读时生成ReadView
RR:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
在这里插入图片描述

当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如select… for update、update、delete(排他锁)都是一种当前读。

快照读

简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,又可能是历史数据,不加锁,是非阻塞读。
RC:每次select,都生成一个快照读
RR:开启事务后第一个select语句才是快照读的地方。

连接查询

在这里插入图片描述

JVM

类加载的流程

加载,验证,准备,解析,初始化。
加载:读取一个class文件,将其转化为某种静态数据结构存储在方法区中,并在堆中生成一个便于用户调用的Java.lang.class类型的对象的过程。在进行类加载时,会在堆里生成一个对应的class对象
验证:验证类是否符合Java规范和JVM规范
准备:为类的静态变量分配内存,初始化为系统的初始值
解析:将符号引用转为直接引用的过程
初始化:为类的静态变量赋予正确的初始值

运行时数据区有哪些组成?能分别说说他们的作用吗?

程序计数器:记住下一条JVM指令执行的地址,是线程私有的,不会存在内存溢出。
有两个栈,都是线程私有的,它的生命周期和线程相同。
虚拟机栈:是描述Java方法运行过程中的内存模型。
本地方法栈:是描述Java本地方法运行过程的内存模型。
堆:是被线程共享的一段区域,在虚拟机启动时创建,目的是存放对象实例。
方法区:与堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据。

什么是双亲委派机制?怎么破坏双亲委派机制?

(1)当加载一个类时,先判断此类是否已经被加载,如果类已经被加载则返回;
(2)如果类没有被加载,则先委托父类加载(父类加载时会判断该类有没有被自己加载过),如果父类加载过则返回,如果没有被加载过则继续向上委托;
(3)如果一直委托都无法加载,子类加载器才会尝试自己加载。
自下往上的类加载器有应用程序类加载器,扩展类加载器和启动类加载器。

只要不依次往上交给父类加载器进行加载,就是打破双亲委派,自定义classLoader,重写loadclass方法。

为什么要使用双亲委派模型?

避免原始类被覆盖的问题,为了防止内存中出现多份同样的字节码

打破双亲委派机制是什么意思?

只要不依次往上交给父加载器进行加载,就是打破双亲委派。自定义ClassLoader,重写loadclass方法。

你知道有哪个场景破坏了双亲委派机制吗?

Tomcat破坏双亲委派机制来达到一个web容器部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,还要能保证每一个应用程序的类库都是独立,相互隔离的效果。所以Tomcat自定义了类的加载器,重写了loadclass方法使其优先加载自己目录下的class文件,来达到class私有的效果。

怎么判断对象不再被使用了呢?

常用的算法有两个引用计数法和可达性分析法
引用计数法思路很简单:当对象被引用则+1,但对象引用失败则-1。当计数器为0时,说明对象不再被引用,可以被可回收
另一种就是可达性分析法:它从GC Roots开始向下搜索,当对象到GC Roots都没有任何引用相连时,说明对象是不可用的,可以被回收
GC Roots是一组必须活跃的引用。从GC Root出发,程序通过直接引用或者间接引用,能够找到可能正在被使用的对象。

垃圾回收的过程是什么样呢?

JVM用的就是可达性分析算法来判断对象是否垃圾。
垃圾回收的第一步就是标记,标记哪些没有被GC Roots引用的对象
标记完之后,我们就可以选择直接清除,只要不被GC Roots关联的,都可以干掉
但是直接清除会有内存碎片的问题,那解决内存碎片的问题也比较简单粗暴,标记完,不直接清除
把标记存活的对象复制到另一块空间,复制完了之后,直接把原有的整块空间给干掉!这样就没有内存碎片的问题了
这种做法缺点又很明显:内存利用率低,得有一块新的区域给我复制(移动)过去
还有一种折中的办法,我未必要有一块大的完整空间才能解决内存碎片的问题,我只要能在当前区域内进行移动
把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛,这种专业的术语就叫做整理

为什么需要分代?

Java虚拟机(JVM)将堆内存分为几个不同的代,这样做的主要原因是为了提高垃圾收集(GC)的效率和性能
大部分对象的生命周期都很短,而只有少部分对象可能会存活很长时间
又由于垃圾回收是会导致stop the world(应用停止访问)
为了使stop the world持续的时间尽可能短以及提高并发式GC所能应对的内存分配速率,所以进行分代。
(但也不是所有的垃圾收集器都会有,只不过我们现在线上用的可能都是JDK8,JDK8及以下所使用到的垃圾收集器都是有分代概念的。)

垃圾回收算法

标记清除算法
标记复制算法
标记整理算法

常见的垃圾回收器

年轻代的垃圾收集器有:Seria、Parallel Scavenge、ParNew
老年代的垃圾收集器有:Serial Old、Parallel Old、CMS
看着垃圾收集器有很多,其实还是非常好理解的。Serial是单线程的,Parallel是多线程
这些垃圾收集器实际上就是实现了垃圾回收算法(标记复制、标记整理以及标记清除算法)
CMS是JDK8之前是比较新的垃圾收集器,它的特点是能够尽可能减少stop the world时间。在垃圾回收时让用户线程和 GC 线程能够并发执行!
又可以发现的是,年轻代的垃圾收集器使用的都是标记复制算法
所以在堆内存划分中,将年轻代划分出Survivor区(Survivor From 和Survivor To),目的就是为了有一块完整的内存空间供垃圾回收器进行拷贝(移动),而新的对象则放入Eden区
下面是堆内存的图,它们的大小是有默认比例的。
在这里插入图片描述

新创建的对象一般是在新生代,那在什么时候会到老年代中呢?

简单可以分为两种情况:
如果对象太大了,就会直接进入老年代(对象创建时就很大 || Survivor区没办法存下该对象)
如果对象太老了,那就会晋升至老年代(每发生一次Minor GC ,存活的对象年龄+1,达到默认值15则晋升老年代 || 动态对象年龄判定 可以进入老年代)

那Minor GC 什么时候会触发呢?

当Eden区空间不足时,就会触发Minor GC

那在Minor GC的时候,从GC Roots出发,那不也会扫描到老年代的对象吗?那不就相当于全堆扫描吗?

JVM里也有解决办法的,HotSpot 虚拟机老的GC(G1以下)是要求整个GC堆在连续的地址空间上,所以会有一条分界线(一侧是老年代,另一侧是年轻代),所以可以通过地址就可以判断对象在哪个分代上,当做Minor GC的时候,从GC Roots出发,如果发现老年代的对象,那就不往下走了(Minor GC对老年代的区域毫无兴趣)

那如果年轻代的对象被老年代引用了呢?(老年代对象持有年轻代对象的引用),那时候肯定是不能回收掉年轻代的对象的,怎么解决呢?

HotSpot虚拟机下 有card table(卡表)来避免全局扫描老年代对象

堆内存的每一小块区域形成卡页,卡表实际上就是卡页的集合。当判断一个卡页中有存在对象的跨代引用时,将这个页标记为脏页

那知道了卡表之后,就很好办了。每次Minor GC 的时候只需要去卡表找到脏页,找到后加入至GC Root,而不用去遍历整个老年代的对象了。

聊一下CMS垃圾回收器吧

如果用Seria和Parallel系列的垃圾收集器:在垃圾回收的时,用户线程都会完全停止,直至垃圾回收结束!
CMS的全称:Concurrent Mark Sweep,翻译过来是并发标记清除
用CMS对比上面的垃圾收集器(Seria和Parallel和parNew):它最大的不同点就是并发:在GC线程工作的时候,用户线程不会完全停止,用户线程在部分场景下与GC线程一起并发执行。
但是,要理解的是,无论是什么垃圾收集器,Stop The World是一定无法避免的!
CMS只是在部分的GC场景下可以让GC线程与用户线程并发执行
CMS的设计目标是为了避免老年代 GC出现长时间的卡顿(Stop The World)

那你清楚CMS的工作流程吗

CMS可以简单分为5个步骤:初始标记、并发标记、(并发预清理)、重新标记以及并发清除
从步骤就不难看出,CMS主要是实现了标记清除垃圾回收算法
1.初始标记的过程(会发生Stop The World)
初始标记会标记GCRoots直接关联的对象以及年轻代指向老年代的对象
初始标记这个过程是会发生Stop The World的。但这个阶段的速度算是很快的,因为没有向下追溯(只标记一层)
2.并发标记的过程(不会发生 Stop The World)
这一阶段主要是从GC Roots向下追溯,标记所有可达的对象。
并发标记在GC的角度而言,是比较耗费时间的(需要追溯)
3.并发标记这个阶段完成之后,就到了并发预处理阶段啦
并发预处理这个阶段主要想干的事情:希望能减少下一个阶段重新标记所消耗的时间,因为下一个阶段重新标记是需要Stop The World的
并发标记这个阶段由于用户线程是没有被挂起的,所以对象是有可能发生变化的
可能有些对象,从新生代晋升到了老年代。可能有些对象,直接分配到了老年代(大对象)。可能老年代或者新生代的对象引用发生了变化…
4.重新标记的过程(会Stop The World)
并发预处理这个阶段阶段结束后,就到了重新标记阶段
重新标记阶段会Stop The World,这个过程的停顿时间其实很大程度上取决于上面并发预处理阶段(可以发现,这是一个追赶的过程:一边在标记存活对象,一边用户线程在执行产生垃圾)
停止用户线程,扫描老年代和年轻代找出存活的老年代对象
5.并发清除的过程(不会Stop The World)
最后就是并发清除阶段,不会Stop The World
一边用户线程在执行,一边GC线程在回收不可达的对象
这个过程,还是有可能用户线程在不断产生垃圾,但只能留到下一次GC 进行处理了,产生的这些垃圾被叫做“浮动垃圾”
完了以后会重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备

GC调优

什么时候触发垃圾回收

当Eden区或S区不够用了
当老年代空间不够用了,
当方法区不够用了,
System.gc(),通知JVM进行垃圾回收。

说一下JVM内存结构

程序计数器:记住下一条JVM指令执行的地址,是线程私有的,不会存在内存溢出。
有两个栈,都是线程私有的,它的生命周期和线程相同。
虚拟机栈:是描述Java方法运行过程中的内存模型。
本地方法栈:是描述Java本地方法运行过程的内存模型。
堆:是被线程共享的一段区域,在虚拟机启动时创建,目的是存放对象实例。
方法区:与堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

请描述一下Java内存模型中的缓存不一致问题?如何解决的?

java内存模型中对于缓存不一致的问题主要是,线程之间的通信先从主存中读取到本地内存,而其他线程读取到的内存是 主存中的数据 出现了可见信问题 导致缓存不一致,可以通过加锁或者使用volatile关键字解决。

G1垃圾回收器的执行流程是如何的?它和CMS相比,有什么优势?它是如何解决漏标问题的?是如何解决跨代引用问题的?

在G1收集器中,可以主要分为Minor GC和Mixed GC,G1的Minor GC和大部分的垃圾收集器都是一样的,等到Eden区满了之后,会触发Minor GC,会发生STW,Minor GC有三个步骤:根扫描,更新&&处理RSet,复制对象。根扫描类似于CMS的初始标记过程,第二步更新RSet,
RSet存储在每个Region中,它记录着其他Region引用了当前Region的对象关系。
我们聊CMS回收过程的时候,同样讲到了Minor GC,它是通过卡表(cart table)来避免全表扫描老年代的对象,
因为Minor GC 是回收年轻代的对象,但如果老年代有对象引用着年轻代,那这些被老年代引用的对象也不能回收掉;
同样的,在G1也有这种问题(毕竟是Minor GC)。CMS是卡表,而G1解决跨代引用的问题的存储一般叫做RSet
第二步就是处理RSet的信息并且扫描,将老年代对象持有年轻代对象的相关引用都加入到GC Roots下,避免被回收掉。
第三步:把扫描之后存活的对象往空的Survivor区或者老年代存放,其他的Eden区进行清除。
当堆空间的占用率达到一定阈值后会触发Mixed GC,Mixed GC依赖全局并发标记统计后的Region数据,全局并发标记过程和CMS非常类似,初始标记(STW),并发标记,最终标记(STW)以及清理(STW),Mixed GC它一定会回收年轻代,并会采集部分老年代的Region进行回收的,所以它是一个混合GC。
初始标记: 只会标记GCRoot直接关联的对象

并发标记: 基于初始标记时标记的对象作为起点, 标记所有(属性)关联的对象

最终标记: 处理漏标问题 (并发标记阶段, 漏标的GCRoot可达的对象)

首先是初始标记,这个过程是共用了Minor GC的STW,复用了扫描GC Roots的操作,在这个过程中,老年代和新生代都会扫。初始标记过程还是比较快的,毕竟没有追溯遍历。
接下来就是并发标记,这个阶段不会发生Stop The World,GC线程与用户线程一起执行,GC线程负责收集各个Region的存活对象信息,从GC Roots往下追溯,查找整个堆存活的对象,比较耗时。
==接下来就到了最终标记阶段,==跟CMS一样,标记那些在并发标记阶段发生变化的对象。
漏标问题:G1解决了并发标记阶段导致引用变更的问题,使用了SATB算法,也就是在GC开始的时候,它为存活的对象做了一次快照,在并发阶段时,把每一次发生的引用关系变化时的旧的引用值给记下来,然后在最终标记阶段只扫描着块发生过变化的引用,看有没有对象还是存活的,加入到GC Roots上。
最后一个阶段就是清理,这个阶段也是会STW的,主要清点和重置标记状态,会根据停顿预测模型(其实就是设定停顿的时间),来决定本次GC回收多少Region。一般来说,Mixed GC会选定所有的年轻代Region,部分回收价值高的老年代Region(回收价值高其实就是垃圾多)进行采集
最后Mixed GC 进行清除还是通过拷贝的方式去干的
所以,一次回收未必是将所有的垃圾进行回收的,G1会依据停顿时间做出选择Region数量。

G1有分区思想,对于CMS及之前的回收器来说,其JVM内存空间按照分代的思路划分成物理连续的一大片区域,但G1回收器中,虽然也采用了分代的思想,但其并没有为其分配一块连续的内存,而是将整块内存化整为零拆分成一个个Region,可以更加灵活的控制GC停顿时间,并且也解决了CMS回收器存在的内存碎片问题以及大内存下的长GC停顿时间问题。
G1用到了标记整理算法,CMS回收器采用的是标记清楚算法,所以会产生非常多的内存碎片。
G1还可以预测停顿时间,能让使用者明确指定在一个长度为M毫秒时间片段内,消耗在垃圾收集上的时间不超过N毫秒
在这里插入图片描述

计算机网络

tcp和udp的区别

tcp:可靠,面向连接,时延大,适用于大文件
udp:不可靠,无连接,时延小,适用于小文件

Https验证过程

为什么需要三次握手?两次不行?

第三次握手主要为了防止已失效的连接请求报文段突然又传输到了服务端,导致产生问题。
比如客户端A发出连接请求,可能因为网络阻塞原因,A没有收到确认报文,于是A再重传一次连接请求。连接成功,等待数据传输完毕后,就释放了连接。
然后A发出的第一个连接请求等到连接释放以后的某个时间才到达服务端B,此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段。
如果不采用三次握手,只要B发出确认,就建立新的连接了,此时A不会响应B的确认且不发送数据,则B一直等待A发送数据,浪费资源。

tcp三次握手

在这里插入图片描述 第一次握手:客户端向服务端发起建立连接请求,客户端会随机生成一个起始序列号x,客户端向服务端发送的字段中包含标志位SYN=1,序列号seq=x。第一次握手前客户端的状态为CLOSE,第一次握手后客户端的状态为SYN-SENT。此时服务端的状态为LISTEN
第二次握手:服务端在收到客户端发来的报文后,会随机生成一个服务端的起始序列号y,然后给客户端回复一段报文,其中包括标志位SYN=1ACK=1,序列号seq=y,确认号ack=x+1。第二次握手前服务端的状态为LISTEN,第二次握手后服务端的状态为SYN-RCVD,此时客户端的状态为SYN-SENT。(其中SYN=1表示要和客户端建立一个连接,ACK=1表示确认序号有效)
第三次握手:客户端收到服务端发来的报文后,会再向服务端发送报文,其中包含标志位ACK=1,序列号seq=x+1,确认号ack=y+1。第三次握手前客户端的状态为SYN-SENT,第三次握手后客户端和服务端的状态都为ESTABLISHED。此时连接建立完成。

四次挥手

在这里插入图片描述
A的应用进程先向其TCP发出连接释放报文段(FIN=1,seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认。
B收到连接释放报文段后即发出确认报文(ACK=1,ack=u+1,seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。
A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。
B发送完数据,就会发出连接释放报文段(FIN=1,ACK=1,seq=w,ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认。
A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL(最大报文段生存时间)后,A才进入CLOSED状态。B收到A发出的确认报文段后关闭连接,若没收到A发出的确认报文段,B就会重传连接释放报文段。

第四次挥手为什么要等待2MSL?

保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在2MSL时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到CLOSED状态,若A在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到CLOSED状态。
防止已失效的连接请求报文段出现在本连接中。A在发送完最后一个ACK报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段。

为什么是四次挥手?

因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。但是在关闭连接时,当Server端收到Client端发出的连接释放报文时,很可能并不会立即关闭SOCKET,所以Server端先回复一个ACK报文,告诉Client端我收到你的连接释放报文了。只有等到Server端所有的报文都发送完了,这时Server端才能发送连接释放报文,之后两边才会真正的断开连接。故需要四次挥手。

tcp连接,为什么握手3次,挥手却需要4次?

HTTPS与HTTP的区别?

  • HTTP是超文本传输协议,信息是明文传输;HTTPS则是具有安全性的ssl加密传输协议。

  • HTTP和HTTPS用的端口不一样,HTTP端口是80,HTTPS是443。

  • HTTPS协议需要到CA机构申请证书,一般需要一定的费用。

  • HTTP运行在TCP协议之上;HTTPS运行在SSL协议之上,SSL运行在TCP协议之上。
    在这里插入图片描述

https是怎么实现的

首先客户端和服务端打招呼,并且把自己支持的TSL版本,加密套件发送给服务端,同时还生成了一个随机数给服务端。接着服务端打招呼,服务端确认支持的TLS版本以及选择的加密套件,并且服务器也生成一个随机数发送给客户端,接着服务端还把证书和公钥发给客户端,都发送完毕就告诉客户端,现在客户端生成随机数,我们称为预主密钥,这个预主密钥不会直接发送出去,而是用刚刚收到的公钥进行加密后再发送出去,客户端这边的TSL协商已经没问题了,加密开始,服务端收到加密后的预主密钥后,会用自己的私钥进行解密,这样服务器就知道预主密钥了,而且只有客户端和服务端知道这预主密钥,因为没有进行直接传输,没有其他人知道这预主密钥是什么,除非私钥被泄露了,最后客户端用预主密钥,第一随机数和第二随机数计算出会话密钥,服务端也用预主密钥,第一随机数和第二随机数计算出会话密钥,各自得到的会话密钥是相同的。前面的步骤都是非对称加密,为了得到这个会话密钥,后面的会话大家都只使用这个会话密钥对数据进行加密,所以后面使用的是对称加密,大家都使用同一个私钥。会话后面不使用非对称加密是因为大家都能看到消耗资源非常大。

首先服务器端已经获取了证书,客户端发出请求,服务器端将证书交给客户端里面有服务器端公钥;
客户端得到服务器端证书,校验是否有效;如果正常,在客户端生成随机数字,用服务器公钥加密此随机字符
加密后发给服务器, 服务器用私钥解开,得到随机字符
两边都有随机字符,生成对称秘钥
服务端和客户端进行 加密后续的数据通信

在这里插入图片描述

集合

集合

看过ArrayList源码吗?

ArrayList底层基于动态数组,支持动态扩容
对外提供的API,支持各种情形的插入和删除

1.ArrayList中维护了一个Object类型的数组elementData
2.当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,通过EnsureCapacityInternal()方法,会给数组分配一个默认的初始容量为10,再调用EnsureExplicitInternal()方法判断是否需要扩容**,如果elementData大小不够,就通过grow()方法进行扩容,新数组的长度=旧数组的长度+旧数组的长度/2,也就是扩大之前的1.5倍,最后使用Arrays.copyof()方法把原先的数组复制过来。
3.如果使用的是指定大小的构造器,则初始elementData
容量为指定大小,如果需要扩容,则直接扩容为elementData为1.5倍。**

简述 Java 集合的相关知识

有三大类:Set、Map、List

数组和ArrayList的区别

数组大小是固定的且无法动态改变,但查询高效
ArrayList的容量可动态增长,但牺牲效率

Arraylist和Linkedlist的区别

ArrayList底层结构是可变数组,增删的效率较低,改查的效率较高。
LinkedList底层结构是双向链表,增删的效率较高,改查的效率较低。

Vector和ArrayList的比较

在这里插入图片描述

HashTable和HashMap的区别

在这里插入图片描述

HashMap 在 JDK7 和 JDK8 的区别,以及他们的实现方式

jdk7中使用数组+链表来实现,jdk8使用的数组+链表+红黑树
jdk7插入在头部,jdk8插入在尾部
在jdk7中,节点是Entry,在jdk8中节点是Node,其实是一样的。
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

扰动函数

扰动函数是为了降低冲突,把hashcode的高16位和低16位进行亦或,如果不亦或,高16都用不上,大部分都会进行低16位的操作,那么冲突就会大大发生
所谓扰动函数指的就是HashMap的hash方法。使用hash方法也就是扰动函数是为了防止一些比较差的hashCode()方法,换句话说,使用扰动函数之后可以减少碰撞,降低冲突。

HashMap底层机制

在这里插入图片描述HashMap是Java中常用的一个数据结构,它使用散列技术来存储键值对。
table表是一个数组,数组中放有链表,每个链表中放的一个元素表示Node,Node又实现了Map.Entry这个接口
1.HashMap底层维护了Node类型的数组table,默认为null
2.当创建对象时,将加载因子初始化为0.75
3.当添加key-value时,通过key的hash值得到在table的索引,然后判断该索引处是否有元素,如果没有元素直接添加,如果该处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,则直接替换value,如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
4.第一次添加,则需要扩容table容量为16,临界值为12
5.以后再扩容,则需要扩容容量为原来的2倍,临界值为原来的2倍,依次类推。
6.在Java8中,如果一条链表的元素个数超过8并且table的大小>=64,就会进行树化。

HashMap1.8 扩容流程

想要了解HashMap的扩容机制,首先要考虑两个问题,一是什么时候才需要扩容,二是HashMap的扩容是什么。
什么时候需要扩容呢,条件有两个,一是当HashMap中的元素个数大于数组长度乘以负载因子时,就会进行扩容,二是当某个链表长度>=8,但是数组存储的结点数没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,节点类型由Node变成TreeNode。
在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化长度为16,以后每次扩容都是达到了扩容阈值(数组长度*0.75)
每次扩容的时候,都是扩容之前容量的2倍
扩容之后,会创建一个新的数组,需要把老数组中的数据挪动到新的数组中。会伴随一次重新hash分配。

1.没有hash冲突的节点,则直接使用e.hash&(newCap-1)计算新数组的索引位置
2.如果是红黑树。走红黑树的添加
3.如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash&oldCap)是否为0,该元素的位置要么停留在原始位置。要么移动到原始位置+增加的数组大小这个位置上。
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

HashMap put过程

在这里插入图片描述put时,HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.

HashMap1.8 get方法流程

get时,HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置,如果当前位置存在元素的话,查看当前位置元素的hash值以及与读取的key是否相同,如果equals为true则直接返回,如果不相同就遍历链上的所有元素,相同返回,都不相同返回Null。

先计算key的hash值,根据 hash值找到对应的数组下标,如果这个位置上没有任何元素,则返回null,如果对应数组下标就是我们要找到的key,则直接找到并返回。如果不是要找的key,则查看后续的节点,如果后续节点是链表节点,则通过循环遍历链表根据key获取value,如果是红黑树节点,则通过调用红黑树的方法根据key获取value。

为什么重写equals方法必需重写HashCode

如果只重写了equals方法,但没有重写hashcode,本来A对象和B对象值相同,B应该覆盖掉A,但是hash值不同,会把两个对象都添加到集合中,造成不可预知的错误。

x.equals(y)==true,但却可有不同的hashcode,这句话对不对,为什么?

不对,hashcode的规范:值相同,必须有相同的hashcode,但相同的hashcode,值不一定相同。

HashMap的数据插入原理是怎样的

在JDK1.7中,HashMap是通过数组加链表的形式实现的,当进行数据插入时,首先判断table表是否为0或length是否为空,如果是,则进行Resize扩容,如果不是,就根据key算出索引值,再判断该索引值上的结点是否为空,如果为空,就直接插入,如果不为空,就判断这个key是否存在,如果存在,则直接覆盖value,如果不存在,就判断这个结点是否是红黑树,如果不是,则遍历链表,判断key是否存在,如果存在,则直接覆盖,如果不存在,就直接加入,加入时,判断链表长度是否大于8,如果大于,就转换为红黑树,如果是红黑树,就在红黑树中直接插入

HashMap怎么设定初始容量大小?如果传10,初始大小会是多少?为什么?
HashMap使用HashMap(int initialCapicity) 进行初始化容量大小,16

为什么HashMap的长度是 2^n次幂?

为了让数据更散列更均匀的分步,更充分利用数组的空间。

为什么经常使用String作为HashMap的Key?

String对象的底层是final修饰的char类型的数组,是不可变的,且重写了hashcode(),这使得,即使两个字符串对象的引用不同,但只要值相同,他们的hashcode就相同,就可以索引到相同的value,直接满足我们hashmap中hash和map的要求。其次,每当创建一个字符串对象时,它的hashcode就会被缓存下来,所以在hashmap储存时不需要再做计算,相比于其他对象快。

你平常在哪使用过HashMap?

HashMap 是 Java 中常用的一种数据结构,它提供了一种键值对(key-value)的存储方式。我可以帮助你理解它的用法以及应用场景。
下面是一些使用 HashMap 的常见场景:

存储用户信息:可以使用 HashMap 来存储用户的信息,其中键可以是一些标识符,如用户ID、用户名等,而值则可以是用户的详细信息,如姓名、年龄、性别等。
缓存数据:HashMap 可以被用作缓存数据的存储,其中键可以是数据的唯一标识符,而值则是缓存的数据。
记录访问日志:可以使用 HashMap 来记录用户访问网站时的行为,其中键可以是用户的IP地址或用户ID,而值则是用户访问的页面或时间戳。
管理菜单项:可以使用 HashMap 来管理网站的菜单项,其中键可以是菜单项的ID,而值则是菜单项的信息。
数据库查询缓存:可以将查询结果存储在 HashMap 中,其中键是查询的参数,而值则是查询结果。这样在下次查询时,可以直接从 HashMap 中获取结果,避免了重复查询数据库的开销。

总之,HashMap 是一种非常灵活的数据结构,可以应用于各种需要存储和检索数据的场景。

多线程

创建线程的方式

继承Thread类,Thread类本质是实现了Runnable接口的一个实例。
Thread类实现了Runnable接口的run()方法。
实现Runnable接口,启动时,不能直接调用start()方法,需要重新实例化一个Thread。
实现Callable接口,如果要获取线程的返回结果的话就实现Callable接口。

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

1.sleep()方法是属于Thread类中的,wait()方法是属于Object()类中的。
2.在调用sleep()方法的过程中,线程不会释放对象锁,而当调用wait()方法时,线程会放弃对象锁,进入等待此对象的等待锁定池(WaitSet),只有针对此对象调用notify()方法后才能被唤醒。
3.wait()方法必须配合锁一起使用,如果直接拿到对象,调用wait()会报错。sleep()方法不需要强制和synchronized配合使用,但wait()需要和synchronized一起用。
4.wait()方法通常用于线程间的交互通信,sleep()通常被用于暂停执行。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

start()方法被用来启动新创建的线程,run()就是一个简单的方法调用,不会启动新线程。

Object类的wait、notify和notifyAll方法有什么作用呢?

为何不是线程调用wait(),而是锁对象调用wait()

由于每个对象都拥有monitor(即锁),所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作,而不是用当前线程来操作,因为当前线程可能会等待多个线程的锁,如果通过线程来操作就非常复杂了。

java中如何实现一个线程呢?如果需要获取线程的返回结果该如何做呢

继承Thread类,Thread类本质是实现了Runnable接口的一个实例。
Thread类实现了Runnable接口的run()方法。
实现Runnable接口,启动时,不能直接调用start()方法,需要重新实例化一个Thread。
实现Callable接口,如果要获取线程的返回结果的话就实现Callable接口

线程执行过程中,抛出一个RuntimeException,线程会是什么状态呢

如果没有try {}catch包裹的话线程会中止

synchronized(连环问)说一说自己对于 synchronized 关键字的了解

synchronized关键字解决的是多个线程之间访问资源的同步性,它可以保证被它修饰的方法或代码块在任意时刻只能有一个线程执行。

说说自己是怎么使用 synchronized 关键字,在项目中用到了吗

synchronized关键字加到static静态方法和代码块上都是给Class类上锁,synchronized关键字加到实例方法上是给对象实例加锁。

说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗

Runnable和Callable的区别

Runnable和Callable都是Java中用来实现多线程的接口。它们都表示可以在一个单独的线程中执行的代码块。然而,它们之间有一些区别。
  Runnable接口只有一个无返回值的run() 方法。它用于定义一个要在单独线程中执行的任务。当线程执行 run()方法时,它将运行任务,但不会返回任何结果。因此, Runnable接口更适合用于不需要返回结果的简单任务。
  Callable接口也是用于定义可以在单独线程中执行的任务,但是它具有不同的方法签名。它的call()方法可以返回一个值,并且可以抛出异常。因此, Callable接口更适合需要返回结果或可能抛出异常的任务。
  Runnable和Callable都是Java中用来实现多线程的接口,他们都表示可以在单独的线程中执行的代码块。
  Runnable接口只有一个无返回值的run()方法,运行时不会返回任何结果。
  Callable接口里的call()方法可以返回一个值,并且可以抛出异常。

线程的状态

新建状态(NEW):当程序使用new关键字创建了一个线程后,该线程就处于新建状态。
可运行状态(RUNNABLE):当线程对象调用start()方法之后,该线程就处于就绪状态。
阻塞状态(BLOCKED):争抢锁时失败的线程就会由可运行状态变为阻塞状态,当持锁线程释放锁时,会唤醒阻塞线程。
等待状态(WAITTING):当线程调用wait()方法时,进入等待状态,调用notify()方法时唤醒线程,
计时等待状态(TIMED-WAITTING):当线程调用sleep(long)时进入计时等待状态,时间到时恢复。或者调用wait(long),时间到或调用notify()方法恢复到可运行状态。
终结状态(TERMINATED):执行完任务后的线程进入终结状态。

AQS底层数据结构以及原理

AQS如何实现公平锁和非公平锁

ReentrantLock中的公平锁和非公平锁的底层实现(默认非公平锁)

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

volatile的含义

volatile是易变关键字,加了volatile修饰的变量,就不能从缓存中读取了,每次必须去主内存中获取变量的最新值
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
volatile一般用于一个变量为修改变量,其他变量为读取变量

volatile能否保证线程安全

线程安全要考虑三个方面:可见性,有序性,原子性
可见性指,一个线程对共享变量修改,另一个线程能看到最新的结果
有序性指,一个线程内代码按编写顺序执行
原子性指,一个线程内多行代码以一个整体运行,期间不能有其他线程的代码插队
volatile能够保证共享变量的可见性和有序性,但并不能保证原子性

volatile可以保证原子性吗

不能,对于i=1这个赋值操作,由于其本身是原子操作,因此在多线程程序中不会出现不一致问题,但是对于i++,i–这种复合操作,即使使用volatile关键字修饰也不能保证操作的原子性,可能会引发数据不一致问题。

synchronized可以保证原子性吗

可以,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

Spring

谈谈你对Spring的理解

Spring控制反转和底层实现

使用框架来创建对象,即将创建对象的权利交给框架。

Spring面向切面编程(AOP)及底层实现方式

AOP简单的说,就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的己有方法进行增强

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

Spring框架中有哪些不同类型的事件

Spring应用程序有哪些不同的组件

使用Spring有哪些方式

Spring注解?

什么是SpringMVC

简单介绍一下你对SpringMVC的理解

SpringMVC的工作原理

MVC框架

SpringMVC常用注解

你们使用的持久层框架是?

为什么使用mybatis?它和hibernate有什么区别?

说一下mybatis怎 么实现一对一和一对多和多对多表关系的?

说说你对ORM的理解

说一下mybatis的缓存机制

spring ioc和aop理解

IOC就是使用框架来创建对象,即将创建对象的权利交给框架。
AOP简单的说,就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的自己的方法进行增强。

aop原理

Spring的AOP其实就是底层使用动态代理来完成的,并且使用了两种动态代理分别是JDK的动态代理和Cglib动态代理.

jdk动态代理和cglib的区别

(代理模式,就是为其他对象提供一种代理以控制这个对象的访问。)
JDK动态代理只能对实现了接口的类生成代理,而不能针对类,生成的代理对象相当于是被代理对象的兄弟。
Cglib的动态代理针对类实现接口,不要求被代理(被增强)的类要实现接口,生成的代理对象相当于被代理对象的子类对象。
Spring的AOP默认情况下优先使用的是JDK的动态代理,如果使用不了JDK的动态代理才会使用Cglib的动态代理。

jdk动态代理

其实就是反射原理,把对象进行反射,然后重写invoke方法,在前面或后面添加一些东西,所以是基于切面编程维度去说的。

Cglib动态代理

Cglib是基于字节码方式去实现的

Spring怎么解决循环依赖

Spring提供了三级缓存存储完整Bean实例和半成品Bean实例,用于解决引用问题。简单来说就是搞了个中间态。

Spring一个接口多次修改数据库怎么保证全部都完成或者全部都失败

实现声明式事务。只需要简单的加个注解(或者是xml配置)就可以实现事务控制。注解是@Transactional

Spring 比较核心的模块

1.Spring core:核心容器
2.Spring AOP:Spring中的面向切面编程
3.Spring ORM(对象关系映射器)对象关系映射模块
4.Spring Web模块
5.Spring MVC
spring的核心功能模块有几个,列举一些重要的spring模块

描述一下 IOC

简述一下 Spring Bean 的生命周期

在这里插入图片描述1.调用bean的构造方法创建Bean

2.通过反射调用setter方法进行属性的依赖注入

3.如果Bean实现了BeanNameAware接口,Spring将调用setBeanName(),设置 Bean的name(xml文件中bean标签的id)

4.如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()把bean factory设置给Bean

5.如果存在BeanPostProcessor,Spring将调用它们的postProcessBeforeInitialization(预初始化)方法,在Bean初始化前对其进行处理

6.如果Bean实现了InitializingBean接口,Spring将调用它的afterPropertiesSet方法,然后调用xml定义的 init-method 方法,两个方法作用类似,都是在初始化 bean 的时候执行

7.如果存在BeanPostProcessor,Spring将调用它们的postProcessAfterInitialization(后初始化)方法,在Bean初始化后对其进行处理

8.Bean初始化完成,供应用使用,这里分两种情况:

8.1 如果Bean为单例的话,那么容器会返回Bean给用户,并存入缓存池。如果Bean实现了DisposableBean接口,Spring将调用它的destory方法,然后调用在xml中定义的 destory-method方法,这两个方法作用类似,都是在Bean实例销毁前执行。

8.2 如果Bean是多例的话,容器将Bean返回给用户,剩下的生命周期由用户控制。

主要步骤简述:
Spring中的bean的生命周期主要包含四个阶段:实例化Bean --> Bean属性填充 --> 初始化Bean -->销毁Bean
实例化 Bean:通过反射调用构造方法实例化对象。
依赖注入:装配 Bean 的属性。
实现了 Aware接口的 Bean,执行接口方法:如顺序执行 BeanNameAware、BeanFactoryAware、ApplicationContextAware的接口方法。
Bean 对象初始化前,循环调用实现了 BeanPostProcessor 接口的预初始化方法(postProcessBeforeInitialization)。
Bean 对象初始化:顺序执行 @PostConstruct 注解方法、InitializingBean 接口方法、init-method 方法。
Bean 对象初始化后,循环调用实现了 BeanPostProcessor 接口的后初始化方法(postProcessAfterInitialization)。
容器关闭时,执行 Bean 对象的销毁方法,顺序是:@PreDestroy 注解方法、DisposableBean 接口方法、destroy-method。

首先是实例化Bean,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚末初始化的依赖时,容器就会调用doCreateBean()方法进行实例化,实际上就是通过反射的方式创建出一个bean对象

Bean实例创建出来后,接着就是给这个Bean对象进行属性填充,也就是注入这个Bean依赖的其它bean对象

Spring Bean的作用域有哪些

Spring中Bean的五个作用域

Spring 是怎么解决循环依赖问题?

Redis

谈谈你对redis的理解?

考察角度:redis是什么?主要有什么用处?有哪些具体的应用场景?性能如何?QPS多少?
Redis是一个高性能的非关系型的键值对数据库,与传统的数据库不同的是Redis是存在内存中的,所以读写速度非常快,每秒可以处理超过10万次的读写操作,这也是Redis常常被用作缓存的原因。

Redis的优缺点?

优点:
读写性能好,读的速度可达110000次/s,写的速度可达81000次/s。
支持数据持久化,有AOF和RDB两种持久化方式 数据结构丰富,支持String、List、Set、Hash等结构
支持事务,Redis所有的操作都是原子性的,并且还支持几个操作合并后的原子性执行,原子性指操作要么成功执行,要么失败不执行,不会执行一部分。
支持主从复制,主机可以自动将数据同步到从机,进行读写分离。
缺点:
因为Redis是将数据存到内存中的,所以会受到内存大小的限制,不能用作海量数据的读写
Redis不具备自动容错和恢复功能,主机或从机宕机会导致前端部分读写请求失败,需要重启机器或者手动切换前端的IP才能切换

Redis的应用场景有哪些?

缓存:Redis基于内存,读写速度非常快,并且有键过期功能和键淘汰策略,可以作为缓存使用。
排行榜:Redis提供的有序集合可以很方便地实现排行榜。
分布式锁:Redis的setnx功能来实现分布式的锁。
社交功能:实现共同好友、共同关注等 计数器:通过String进行自增自减实现计数功能
消息队列:Redis提供了发布、订阅、阻塞队列等功能,可以实现一个简单的消息队列。

Redis为什么这么快?

Redis为什么这么快主要有以下几个原因:
运行在内存中
数据结构简单
使用多路IO复用技术
单线程实现,单线程避免了线程切换、锁等造成的性能开销。

为什么Redis这么快?单线程为什么变成多线程

考察角度:redis的线程模型,还有IO多路复用

采用了IO多路复用,能够快速的识别哪些客户端发送数据到内核上

Redis6.0之前是单线程的,为什么Redis6.0之前采用单线程而不采用多线程呢?

简单来说,就是Redis官方认为没必要,单线程的Redis的瓶颈通常在CPU的IO,而在使用Redis时几乎不存在CPU成为瓶颈的情况。使用Redis主要的瓶颈在内存和网络,并且使用单线程也存在一些优点,比如系统的复杂度较低,可为维护性较高,避免了并发读写所带来的一系列问题。
在这里插入图片描述

Redis6.0之后为什么引入了多线程?

Redis的瓶颈在内存和网络,Redis6.0引入多线程主要是为了解决网路IO读写这个瓶颈,执行命令还是单线程执行的,所以也不存在线程安全问题。
Redis6.0默认是否开启了多线程呢?
默认是没有开启的,如需开启,需要修改配置文件redis.conf:io-threads-do-reads no,no改为yes

讲讲redis的一些常用指令。

考察角度:常用的API,通过这个看你到底用没用过,都用redis做过什么东西?最好提前准备。

Redis的数据类型有哪些?

Redis的常见的数据类型有String、Hash、Set、List、ZSet。还有三种不那么常见的数据类型:Bitmap、HyperLogLog、Geospatial。

在这里插入图片描述

Redis键的过期删除策略

常见的过期删除策略是惰性删除、定期删除、定时删除。
惰性删除:只有访问这个键时才会检查它是否过期,如果过期则清除。优点:最大化地节约CPU资源。缺点:如果大量过期键没有被访问,会一直占用大量内存。
定时删除:为每个设置过期时间的key都创造一个定时器,到了过期时间就清除。优点:该策略可以立即清除过期的键。缺点:会占用大量的CPU资源去处理过期的数据。
定期删除:每隔一段时间就对一些键进行检查,删除其中过期的键。该策略是惰性删除和定时删除的一个折中,既避免了占用大量CPU资源又避免了出现大量过期键不被清除占用内存的情况。
Redis中同时使用了惰性删除和定期删除两种。

Redis的内存淘汰机制是什么样的?

Redis是基于内存的,所以容量肯定是有限的,有效的内存淘汰机制对Redis是非常重要的。
当存入的数据超过Redis最大允许内存后,会触发Redis的内存淘汰策略。在Redis4.0前一共有6种淘汰策略。

volatile-lru:当Redis内存不足时,会在设置了过期时间的键中使用LRU算法移除那些最少使用的键。(注:在面试中,手写LRU算法也是个高频题,使用双向链表和哈希表作为数据结构)
volatile-ttl:从设置了过期时间的键中移除将要过期的
volatile-random:从设置了过期时间的键中随机淘汰一些
allkeys-lru:当内存空间不足时,根据LRU算法移除一些键
allkeys-random:当内存空间不足时,随机移除某些键
noeviction:当内存空间不足时,新的写入操作会报错
前三个是在设置了过期时间的键的空间进行移除,后三个是在全局的空间进行移除

在Redis4.0后可以增加两个
volatile-lfu:从设置过期时间的键中移除一些最不经常使用的键(LFU算法:Least Frequently Used))
allkeys-lfu:当内存不足时,从所有的键中移除一些最不经常使用的键 这两个也是一个是在设置了过期时间的键的空间,一个是在全局空间。

什么是Redis的持久化?

因为Redis是基于内存的,为了让防止一些意外情况导致数据丢失,需要将数据持久化到磁盘上。

redis持久化方式

RDB方式和AOF方式

RDB

RDB是redis默认的持久化方式,按照一定的时间间隔将内存的数据以快照的形式保存到硬盘,恢复时是将快照读取到内存中。RDB持久化实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。如下图
在这里插入图片描述优点:
适合对大规模的数据恢复,比AOF的启动效率高
只有一个文件 dump.rdb,方便持久化
性能最大化,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
缺点:
数据安全性低,在一定间隔时间内做一次备份,如果Redis突然宕机,会丢失最后一次快照的修改
由于RDB是通过fork子进程来协助完成数据持久化工作的,因此当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
在这里插入图片描述
优点:
具备更高的安全性,Redis提供了3种同步策略,分别是每秒同步、每修改同步和不同步。相比RDB突然宕机丢失的数据会更少,每秒同步会丢失一秒种的数据,每修改同步会不会丢失数据。
由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。
AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作,可以通过该文件完成数据的重建。
缺点:
对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
根据AOF选择同步策略的不同,效率也不同,但AOF在运行效率上往往会慢于RDB。

什么是Redis的事务

Redis的事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断,所以Redis事务是在一个队列中,一次性、顺序性、排他性地执行一系列命令。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。

Redis事务的相关命令

Redis事务相关的命令主要有以下几种:
DISCARD:命令取消事务,放弃执行事务队列内的所有命令,恢复连接为非 (transaction) 模式,如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH
EXEC:执行事务队列内的所有命令。
MULTI:用于标记一个事务块的开始。
UNWATCH:用于取消 WATCH命令对所有 key 的监视。如果已经执行过了EXEC或DISCARD命令,则无需再执行UNWATCH命令,因为执行EXEC命令时,开始执行事务,WATCH命令也会生效,而 DISCARD命令在取消事务的同时也会取消所有对 key 的监视,所以不需要再执行UNWATCH命令了
WATCH:用于标记要监视的key,以便有条件地执行事务,WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。

Redis事务的特性,Redis事务执行的三个阶段

Redis事务的特性
1.Redis事务不保证原子性,单条的Redis命令是原子性的,但事务不能保证原子性。
2.Redis事务是有隔离性的,但没有隔离级别,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。(顺序性、排他性)
3.Redis事务不支持回滚,Redis执行过程中的命令执行失败,其他命令仍然可以执行。(一次性) Redis事务执行的三个阶段 开始事务(MULTI) 命令入列 执行事务(EXEC)
Redis事务执行的三个阶段
开始事务(MULTI)
命令入列
执行事务(EXEC)

Redis事务为什么不支持回滚?

在Redis事务中,命令允许失败,但是Redis会继续执行其它的命令而不是回滚所有命令,是不支持回滚的。
主要原因有以下两点:
Redis 命令只在两种情况失败:
语法错误的时候才失败(在命令输入的时候不检查语法)。
要执行的key数据类型不匹配:这种错误实际上是编程错误,这应该在开发阶段被测试出来,而不是生产上。
因为不需要回滚,所以Redis内部实现简单并高效。(在Redis为什么是单线程而不是多线程也用了这个思想,实现简单并且高效)

Redis并发竞争key问题应该如何解决?

Redis并发竞争key就是多个客户端操作一个key,可能会导致数据出现问题,主要有以下几种解决办法:(主要1.2.)
1.乐观锁,watch 命令可以方便的实现乐观锁。watch 命令会监视给定的每一个key,当 exec 时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。不能在分片集群中使用
2.分布式锁,适合分布式场景
3.时间戳,适合有序场景,比如A想把key设置为1,B想把key设置为2,C想把key设置为3,对每个操作加上时间戳,写入前先比较自己的时间戳是不是早于现有记录的时间戳,如果早于,就不写入了
4.消息队列,串行化处理

缓存雪崩

下图是一个正常的系统架构图,其中缓存的作用是减轻数据库的压力,提升系统的性能,无论是缓存雪崩、缓存击穿还是缓存穿透都是缓存失效了导致数据库压力过大。
在这里插入图片描述

什么是缓存雪崩?

缓存雪崩是指在某一个时刻出现大规模的缓存失效的情况,大量的请求直接打在数据库上面,可能会导致数据库宕机,如果这时重启数据库并不能解决根本问题,会再次造成缓存雪崩。

为什么会造成缓存雪崩?

一般来说,造成缓存雪崩主要有两种可能
Redis宕机了
很多key采取了相同的过期时间

如何解决缓存雪崩?

为避免Redis宕机造成缓存雪崩,可以搭建Redis集群
尽量不要设置相同的过期时间,例如可以在原有的过期时间加上随机数
服务降级,当流量到达一定的阈值时,就直接返回“系统繁忙”之类的提示,防止过多的请求打在数据库上,这样虽然难用,但至少可以使用,避免直接把数据库搞挂

缓存击穿

什么是缓存击穿?

缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增,这种现象就叫做缓存击穿。
比较经典的例子是商品秒杀时,大量的用户在抢某个商品时,商品的key突然过期失效了,所有请求都到数据库上了。

如何解决缓存击穿

考虑热点key不设置过期时间,避免key过期失效
加锁,如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库宕机,不过这样会导致系统的性能变差。

缓存穿透

什么是缓存穿透

缓存穿透是指用户的请求没有经过缓存而直接请求到数据库上了,比如用户请求的key在Redis中不存在,或者用户恶意伪造大量不存在的key进行请求,都可以绕过缓存,导致数据库压力太大挂掉。

如何解决缓存穿透

参数校验,例如可以对用户id进行校验,直接拦截不合法的用户的请求
布隆过滤器,布隆过滤器可以判断这个key在不在数据库中,特点是如果判断这个key不在数据库中,那么这个key一定不在数据库中,如果判断这个key在数据库中,也不能保证这个key一定在数据库中。就是会有少数的漏网之鱼,造成这种现象的原因是因为布隆过滤器中使用了hash算法,对key进行hash时,不同的key的hash值一定不同,但相同的hash的值不能说明这两个key相同。下面简单介绍下布隆过滤器,这个面试也常问。
布隆过滤器底层使用bit数组存储数据,该数组中的元素默认值是0。

布隆过滤器第一次初始化的时候,会把数据库中所有已存在的key,经过一系列的hash算法计算,算出每个key的位置,并将该位置的值置为1,为了减少哈希冲突的影响,可以对每个key进行多次hash计算,如下图

现在,用户所有的请求都要经过布隆过滤器过滤一遍,如果只有用户请求的key的hash值都是1才可以通过,否则直接拦截,如下图

如何保证缓存与数据库双写时的数据一致性?

这是面试的高频题,需要好好掌握,这个问题是没有最优解的,只能数据一致性和性能之间找到一个最适合业务的平衡点
首先先来了解下一致性,在分布式系统中,一致性是指多副本问题中的数据一致性。一致性可以分为强一致性、弱一致性和最终一致性

强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。强一致性对用户比较友好,但对系统性能影响比较大。
弱一致性:系统并不保证后续进程或者线程的访问都会返回最新的更新过的值。
最终一致性:也是弱一致性的一种特殊形式,系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。
大多数系统都是采用的最终一致性,最终一致性是指系统中所有的副本经过一段时间的异步同步之后,最终能够达到一个一致性的状态,也就是说在数据的一致性上存在一个短暂的延迟。

如果想保证缓存和数据库的数据一致性,最简单的想法就是同时更新数据库和缓存,但是这实现起来并不现实,常见的方案主要有以下几种:

先更新数据库,后更新缓存
先更新缓存,后更新数据库 先更新数据库,后删除缓存
先删除缓存,后更新数据库
乍一看,感觉第一种方案就可以实现缓存和数据库一致性,其实不然,更新缓存是个坑,一般不会有更新缓存的操作。因为很多时候缓存中存的值不是直接从数据库直接取出来放到缓存中的,而是经过一系列计算得到的缓存值,如果数据库写操作频繁,缓存也会频繁更改,所以更新缓存代价是比较大的,并且更改后的缓存也不一定会被访问就又要重新更改了,这样做无意义的性能消耗太大了。下面介绍删除缓存的方案

Redis回收使用什么算法?

Redis回收使用LRU算法和引用计数法

LRU算法很常见,在学习操作系统时也经常看到,淘汰最长时间没有被使用的对象,LRU算法在手撕代码环节也经常出现,要提前背熟

引用计数法在学习JVM中也见过的,对于创建的每一个对象都有一个与之关联的计数器,这个计数器记录着该对象被使用的次数,当对象被一个新程序使用时,它的引用计数值会被增1,当对象不再被一个程序使用时,它的引用计数值会被减1,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,简单来说就是淘汰使用次数最少的对象(LFU算法)

Redis 里面有1亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?

可以使用Redis的KEYS命令,用于查找所有匹配给定模式 pattern 的 key ,虽然时间复杂度为O(n),但常量时间相当小。

注意: 生产环境使用 KEYS命令需要非常小心,在大的数据库上执行命令会影响性能,KEYS指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个命令适合用来调试和特殊操作,像改变键空间布局。

不要在你的代码中使用 KEYS 。如果你需要一个寻找键空间中的key子集,考虑使用 SCAN 或 sets。

Redis集群的实现方案有哪些?

在说Redis集群前,先说下为什么要使用Redis集群,Redis单机版主要有以下几个缺点:

不能保证数据的可靠性,服务部署在一台服务器上,一旦服务器宕机服务就不可用。
性能瓶颈,内存容量有限,处理能力有限
Redis集群就是为了解决Redis单机版的一些问题,Redis集群主要有以下几种方案

Redis 主从模式
Redis 哨兵模式
Redis 自研
Redis Clustert
下面对这几种方案进行简单地介绍:

Redis主从模式

Redis单机版通过RDB或AOF持久化机制将数据持久化到硬盘上,但数据都存储在一台服务器上,并且读写都在同一服务器(读写不分离),如果硬盘出现问题,则会导致数据不可用,为了避免这种问题,Redis提供了复制功能,在master数据库中的数据更新后,自动将更新的数据同步到slave数据库上,这就是主从模式的Redis集群,如下图

在这里插入图片描述

主从模式解决了Redis单机版存在的问题,但其本身也不是完美的,主要优缺点如下:

优点

高可靠性,在master数据库出现故障后,可以切换到slave数据库
读写分离,slave库可以扩展master库节点的读能力,有效应对大并发量的读操作 缺点: 不具备自动容错和恢复能力,主节点故障,从节点需要手动升为主节点,可用性较低
缺点

不具备自动容错和恢复能力,主节点故障,从节点需要手动升为主节点,可用性较低

主从集群模式

主从集群,主从库之间采用的是读写分离
主库:所有的写操作都在写库发生,然后主库同步数据到从库,同时也可以进行读操作;
从库:只负责读操作;
在这里插入图片描述
主库需要复制数据到从库,主从双方的数据库需要保存相同的数据,将这种情况称为”数据库状态一致”

Redis 哨兵模式

为了解决主从模式的Redis集群不具备自动容错和恢复能力的问题,Redis从2.6版本开始提供哨兵模式

哨兵模式的核心还是主从复制,不过相比于主从模式,多了一个竞选机制(多了一个哨兵集群),从所有从节点中竞选出主节点,如下图
在这里插入图片描述
从上图中可以看出,哨兵模式相比于主从模式,主要多了一个哨兵集群,哨兵集群的主要作用如下:

监控所有服务器是否正常运行:通过发送命令返回监控服务器的运行状态,处理监控主服务器、从服务器外,哨兵之间也相互监控。
故障切换:当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换master。同时那台有问题的旧主也会变为新主的从,也就是说当旧的主即使恢复时,并不会恢复原来的主身份,而是作为新主的一个从。
哨兵模式的优缺点:

优点

哨兵模式是基于主从模式的,解决可主从模式中master故障不可以自动切换故障的问题。
缺点

浪费资源,集群里所有节点保存的都是全量数据,数据量过大时,主从同步会严重影响性能
Redis主机宕机后,投票选举结束之前,谁也不知道主机和从机是谁,此时Redis也会开启保护机制,禁止写操作,直到选举出了新的Redis主机。
只有一个master库执行写请求,写操作会单机性能瓶颈影响

哨兵机制

对于主从集群模式,如果从库发生了故障,还有主库和其它的从库可以接收请求,但是如果主库挂了,就不能进行正常的数据写入,同时数据同步也不能正常的进行了,当然这种情况,我们需要想办法避免,于是就引入了下面的哨兵机制。

什么是哨兵机制

sentinel(哨兵机制):是 Redis 中集群的高可用方式,哨兵节点是特殊的 Redis 服务,不提供读写,主要来监控 Redis 中的实例节点,如果监控服务的主服务器下线了,会从所属的从服务器中重新选出一个主服务器,代替原来的主服务器提供服务。
在这里插入图片描述
核心功能就是:监控,选主,通知。

监控:哨兵机制,会周期性的给所有主服务器发出 PING 命令,检测它们是否仍然在线运行,如果在规定的时间内响应了 PING 通知则认为,仍在线运行;如果没有及时回复,则认为服务已经下线了,就会进行切换主库的动作。

选主:当主库挂掉的时候,会从从库中按照既定的规则选出一个新的的主库,

通知:当一个主库被新选出来,会通知其他从库,进行连接,然后进行数据的复制。当客户端试图连接失效的主库时,集群也会向客户端返回新主库的地址,使得集群可以使用新的主库。

Redis Cluster

前面介绍了为了解决哨兵模式的问题,各大企业提出了一些数据分片存储的方案,在Redis3.0中,Redis也提供了响应的解决方案,就是Redist Cluster。

Redis Cluster是一种服务端Sharding技术,Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。

什么是Redis哈希槽呢?本来不想详细介绍这个的,但面试确实经常问,还是简单说一下,

在介绍slot前,要先介绍下一致性哈希(客户端分片经常会用的哈希算法),那这个一致性哈希有什么用呢?其实就是用来保证节点负载均衡的,那么多主节点,到底要把数据存到哪个主节点上呢?就可以通过一致性哈希算法要把数据存到哪个节点上。

一致性哈希

下面详细说下一致性哈希算法,首先就是对key计算出一个hash值,然后对2^32取模,也就是值的范围在[0, 2^32 -1],一致性哈希将其范围抽象成了一个圆环,使用CRC16算法计算出来的哈希值会落到圆环上的某个地方。

现在将Redis实例也分布在圆环上,如下图
在这里插入图片描述

假设A、B、C三个Redis实例按照如图所示的位置分布在圆环上,通过上述介绍的方法计算出key的hash值,发现其落在了位置E,按照顺时针,这个key值应该分配到Redis实例A上。

如果此时Redis实例A挂了,会继续按照顺时针的方向,之前计算出在E位置的key会被分配到RedisB,而其他Redis实例则不受影响。

但一致性哈希也不是完美的,主要存在以下问题:当Redis实例节点较少时,节点变化对整个哈希环中的数据影响较大,容易出现部分节点数据过多,部分节点数据过少的问题,出现数据倾斜的情况,如下图,数据落在A节点和B节点的概率远大于C节点
在这里插入图片描述

为了解决这种问题,可以对一致性哈希算法引入虚拟节点(A#1,B#1,C#1),如下图

在这里插入图片描述
那这些虚拟节点有什么用呢?每个虚拟节点都会映射到真实节点,例如,计算出key的hash值后落入到了位置D,按照顺时针顺序,应该落到节点C#1这个虚拟节点上,因为虚拟节点会映射到真实节点,所以数据最终存储到节点C。

Redi集群最大的节点个数是多少?为什么?

16384个

Redis作者的回答主要说了两点:

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 8 (8 bit) 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 8 (8 bit) 1024(1k) =65K),也就是说需要8k的心跳包,作者认为这样做不太值得

由于其他设计折衷,一般情况下一个redis集群不会有超过1000个master节点

MyBatis

mybatis中$和#区别?

#{}就是占位符,即预编译,$ {}是字符串替换符,即sql拼接。#{}很大程度上能防止sql注入,${}不能防止sql注入。#{}会将其变量两边加单引号,处理 $ {}符号时会直接引入变量,不加引号。MyBatis排序时使用order by 动态参数时,用 $ {}而不是#{}

未解决

将一个对象作为hashmap的key,可能会出现什么问题?怎么解决?

LinkedHashMap是如何实现LRU的?

删除ArrayList中的偶数,给思路(不能从前到后for循环遍历remove删除,可以使用迭代器或者从后往前遍历删除)

HashMap get方法,如果发生Hash冲突,怎么找到想要的key,用什么方法比较的

能说出一个HashMap的使用场景吗

HashMap的哈希函数设计是怎样的?

MySQL 的索引有哪几种类型?

用的是什么树?

Redis 的数据结构有哪些?(常用的)

聊聊spring和springboot上的区别

IOC和AOP 这两个是什么关系

你知道统一异常管理怎么做吗

讲讲声明一个bean有哪些注解

注入一个bean有哪些方式

同步跟异步有什么区别

多线程有什么优点和缺点
创建一个线程有哪些方式
知道怎么创建多线程吗
常见的Linux命令:ls是什么、cd、vi、grep、怎么退出vi编辑器
Mysql的查询很慢,你会从哪几个方面优化它(不考虑数据量的问题)就对SQL优化,(面试官提示:从索引方面来)
你知道索引失效吗
break和continue的作用和区别
==和equals的区别 要基于基本类型和引用类型来说
linkedlist和ArrayList的区别
map的话有几种
list的jdk8的新特性用过吗(其实就是流操作)具体用过哪些

自我介绍
list set map 区别
list,set 详谈
hash表
对象拷贝(深,浅)
synchronized 和lock 详谈;如何选择?
MySQL索引
数据库事务
线程池工作原理和机制,拒绝策略
说说浏览器输入一个网址到显示的过程
自我介绍
数据库的隔离级别有哪些,然后具体讲讲有什么区别怎么实现的
怎么设计索引
最左匹配原则是什么,以及为什么这样就可以用到联合索引
讲讲索引的数据结构
怎么排查慢sql
讲讲java锁升级的过程
讲讲AQS是什么
讲讲一个http请求发到controller的过程
两千万个数据,数据上限最大是40亿,如何找到两个不重复的

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值