JAVA面试+Redis

JAVA面试

一、Java基础篇

1.0 pojo实体类使用Integer与int的区别

①:Integer的默认值是null,而int的默认值是0(无法接受nullz)。数据库中的INT类型对应的是Integer,而不是int。
定义属性值int类型的时候,在数据库中默认null,当插入操作的时候会把默认值变成0。
定义属性值Integer类型的时候,在数据库中默认也是null,但是当插入操作的的时候默认值会变成null。所以在后端对该属性进行判断是否为空时,需注意二者的区别,一个判断是否为null,一个判断是否为0。但在实际的应用中有些类型的值为0是有意义的,如 gender(性别):0(代表女)1(代表男),此时当插入的数据为空时,int就会插入0,产生歧义。
②:前端传来的json格式数据如果为空,则int无法接收

1.接口和抽象类的区别

相似点:

(1)接口和抽象类都不能被实例化

(2)实现接口或继承抽象类的普通子类都必须实现这些抽象方法

不同点:

(1)抽象类可以包含普通方法和代码块和私有方法;接口里只能包含抽象方法,静态方法(必须有方法体)和默认方法(必须含有方法体),不能含有私有方法

(2)抽象类可以有构造方法,而接口没有

(3)抽象类中的成员变量可以是各种类型的,接口的成员变量只能是 public static final 类型的,并且必须赋值

2.重载和重写的区别

重载发生在同一个类中,方法名相同、参数列表、返回类型、权限修饰符可以不同

重载的特点:与修饰符、返回值类型无关,只看参数列表,方法名相同且参数列表必须不同。(参数个数或参数类型)。调用时,根据方法参数列表的不同来区别。

重写发生在子类中,方法名相、参数列表、返回类型都相同,权限修饰符要大于父类方法,声明异常范围要小于父类方法,但是final和private修饰的方法不可重写

3.==和equals的区别

==比较①:基本类型,比较的是值,②比较引用类型,比较的是内存地址,equlas是Object类的方法,本质上与一样,但是有些类重写了equals方法,比如String的equals被重写后,比较的是字符值,另外重写了equlas后,也必须重写hashcode()方法

4.异常处理机制

try{
	......	//可能产生异常的代码
}
catch( 异常类型1 e ){
	......	//当产生异常类型1型异常时的处置措施
}
catch( 异常类型2 e ){
	...... 	//当产生异常类型2型异常时的处置措施
}  
finally{
	...... //无论是否发生异常,都无条件执行的语句
} 

(1)使用try、catch、finaly捕获异常,finaly中的代码一定会执行,捕获异常后,try后面的代码不会执行,但是整个try…catch之后的代码可以继续运行(try块中的代码发生了异常,但是所有catch分支都无法匹配(捕获)这个异常,那么JVM将会终止当前方法的执行,并把异常对象“抛”给调用者。如果调用者不处理,程序就挂了)

(2)使用throws声明该方法可能会抛出的异常类型,出现异常后,程序终止

(3)不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。

  • 唯一的例外,使用 System.exit(0) 来终止当前正在运行的 Java 虚拟机;且finally不能被单独使用

5.HashMap原理

底层+特点+扩容

1.HashMap在Jdk1.8以后是基于数组+链表+红黑树来实现的,特点是,key不能重复,可以为null,线程不安全

2.HashMap的扩容机制:

HashMap的默认容量为16,默认的负载因子为0.75,当HashMap中元素个数超过容量乘以负载因子的个数时,就创建一个大小为前一次两倍的新数组,再将原来数组中的数据复制到新数组中。当数组长度到达64且链表长度大于8时,链表转为红黑树

3.HashMap存取原理:

(1)计算key的hash值,然后进行二次hash,根据二次hash结果找到对应的索引位置

(2)如果这个位置有值,先进行equals比较key的值,若结果为true则取代该元素,若结果为false(则使用链表或者红黑树解决哈希冲突),则采用了优化后的"高位运算法"或者"红黑树"来解决哈希冲突,以避免并发环境下的数据丢失等问题。

6.想要线程安全的HashMap怎么办?

(1)使用ConcurrentHashMap

(2)使用HashTable

(3)Collections.synchronizedHashMap()方法

7.ConcurrentHashMap如何保证的线程安全?

Java 8中的ConcurrentHashMap通过结合分段锁、CAS和synchronized的灵活使用,以及volatile关键字的应用,确保了高效且线程安全的并发访问。这使得ConcurrentHashMap成为了在多线程环境下高度并发的数据结构选择。

分段锁:ConcurrentHashMap被划分为多个段(Segment),每个段维护着一个独立的哈希表,这些段之间相互独立,不会相互阻塞。在默认情况下,ConcurrentHashMap被划分为16个段(可以通过构造函数进行调整),因此最多支持同时进行16个写操作。当进行读操作时,不需要获得锁。这样,在大部分场景下,多个读操作可以同时进行,提高了并发性能。
CAS和synchronized的组合使用:在访问段内的元素时,使用CAS操作进行更新。但如果CAS操作失败,会退化为synchronized方式,即需要获得段级别的锁来确保线程安全。这种组合可以在大部分情况下使用非阻塞的CAS进行更新操作,只有在必要的情况下才会使用锁。
volatile修饰的字段:ConcurrentHashMap中有一些字段(如Segment中的count值)使用了volatile关键字修饰,以保证其对所有线程的可见性。
带有预计算的扩容:在ConcurrentHashMap进行扩容时,会提前计算出新的容量,并将原先的元素重新分配到新的段中。这样可以减少扩容过程中对旧元素的访问次数,从而减少了锁的竞争。
CAS指令是一种原子操作,通过比较当前值和期望值是否相等来进行更新。当期望值等于当前值时,CAS会使用新的值更新当前值。否则,不进行任何操作。CAS常用于实现非阻塞算法和乐观锁,并且可以保证并发情况下的原子性、可见性和有序性。

Synchronized关键字是Java中最基本的同步机制,它可以将代码块或方法标记为同步代码块或同步方法。在执行同步代码块或同步方法时,会首先尝试获取对象的监视器锁,如果获取成功,则进入同步块或同步方法;否则,线程就会阻塞等待获取锁。Synchronized能够解决原子性、可见性和有序性问题,并且还能够保证线程的互斥访问。

8.HashTable与HashMap的区别

(1)HashTable的每个方法都用synchronized修饰,因此是线程安全的,但同时读写效率很低

(2)HashTable的Key不允许为null

(3)HashTable只对key进行一次hash,HashMap进行了两次Hash

(4)HashTable底层使用的数组加链表

9.ArrayList和LinkedList的区别

ArratList的底层使用动态数组,默认容量为10,当元素数量到达容量时,生成一个新的数组,大小为前一次的1.5倍,然后将原来的数组copy过来;因为数组在内存中是连续的地址,所以ArrayList查找数据更快,由于扩容机制添加数据效率更低

LinkedList的底层使用链表,在内存中是离散的,没有扩容机制;LinkedList在查找数据时需要从头遍历,所以查找慢,但是添加数据效率更高

10.如何保证ArrayList的线程安全?

(1)使用collentions.synchronizedList()方法为ArrayList加锁

(2)使用Vector,Vector底层与Arraylist相同,但是每个方法都由synchronized修饰,速度很慢

(3)使用juc下的CopyOnWriterArrayList,该类实现了读操作不加锁,写操作时为list创建一个副本,期间其它线程读取的都是原本list,写操作都在副本中进行,写入完成后,再将指针指向副本。

10.1.CopyOnWriterArrayList

CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不过 CopyOnWriteArrayList 在对数组进行操作的时候,基本会分三步走:

加锁;
在进行写操作(添加、修改、删除元素)时,并不直接在原始数据上进行操作,而是先将原始数据进行复制,然后在复制的数据上进行修改。这样做的好处是,读操作不会被阻塞,因为多个线程可以同时对原始数据进行读取,提高了读操作的性能。
解锁

除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到

不适合的场景:

内存占用问题:由于每次写操作都会进行数据复制,因此在进行频繁的写操作时,会占用大量的内存空间。如果应用程序中有大量的写操作,则不适合使用CopyOnWriteArrayList。
不适合实时性要求高的场景:由于读操作可能会看到更新前的旧数据,所以不适合对实时性要求较高的场景。

11.String、StringBuffer、StringBuilder的区别

String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。

StringBuffer可变并且线程安全

StringBuiler可变但线程不安全。

操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据用 StringBuffer。

12.hashCode和equals

hashCode()和equals()都是Object类的方法,hashCode()默认是通过地址来计算hash码,但是可能被重写过用内容来计算hash码,equals()默认通过地址判断两个对象是否相等,但是可能被重写用内容来比较两个对象

所以两个对象相等,他们的hashCode和equals一定相等,但是hashCode相等的两个对象未必相等

如果重写equals()必须重写hashCode(),比如在HashMap中,key如果是String类型,String如果只重写了equals()而没有重写hashcode()的话,则两个equals()比较为true的key,因为hashcode不同导致两个key没有出现在一个索引上,就会出现map中存在两个相同的key

13.面向对象和面向过程的区别

面向对象:

面向对象编程(OOP)是一种将问题分解为更小、更易于管理的部分的方法。这种方法强调的是“对象”和“类”,对象是具有特定属性和行为的事物,类是对象的模板或蓝图。在面向对象编程中,问题被视为一组相互作用的对象,每个对象具有自己的状态(属性)和行为(方法)。对象通过发送消息来进行通信,通过类的实例化创建对象。面向对象编程提供了更高层次的抽象,使得代码结构更清晰、易于理解和维护,适用于大型复杂的系统开发。

面向过程编程(PP)则是将注意力集中在解决问题的步骤上。在这种方法中,代它将问题分解为一系列的步骤或函数,通过按照特定顺序执行这些步骤来解决问题。面向过程编程注重算法和函数的设计,强调过程间的函数调用和数据流动。它的控制结构通常是线性的,适合解决简单和直接的问题。

面向对象编程提供了更高级别的抽象、灵活性和可重用性,适合处理复杂的问题和大型系统开发。面向过程编程则更注重算法和步骤的设计,适合解决简单和直接的问题。选择使用哪种编程范式取决于具体问题的需求和复杂性。

面向对象有封装、继承、多态性的特性,所以相比面向过程易维护、易复用、易扩展,但是因为类调用时要实例化,所以开销大性能比面向过程低

14.深拷贝和浅拷贝

浅拷贝:浅拷贝只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存
深拷贝:深拷贝会创造一个一摸一样的对象,新对象和原对象不共享内存,修改新对象不会改变原对对象。

16.什么是反射?

反射是通过获取类的class对象,然后动态的获取到这个类的内部结构,动态的去操作类的属性和方法。
应用场景有:要操作权限不够的类属性和方法时、实现自定义注解时、动态加载第三方jar包时、按需加载类,节省编译和初始化时间;
获取class对象的方法有:class.forName(类路径),类.class(),对象的getClass()

17.Java创建对象得五种方式?

(1)new关键字 (2)Class.newInstance (3)Constructor.newInstance

(4)Clone方法 (5)反序列化

18.方法

  • 方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数过程

  • 将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码

  • Java里的方法不能独立存在,所有的方法必须定义在类里。

[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表]{
        方法体的功能代码
}

19.continue、break 和 return 的区别是什么?

在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词:

  1. continue:指跳出当前的这一次循环,继续下一次循环。
  2. break:指跳出整个循环体,继续执行循环下面的语句。

return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:

  1. (结束一个方法)如果返回值类型不是void,方法体中必须保证一定有 return 返回值 ,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。
  2. (结束一个方法的同时,可以返回数据给方法的调用者 )如果返回值类型为void时,方法体中可以没有return语句,如果要用return语句提前结束方法的执行,那么return后面不能跟返回值,直接写return 就可以。
  3. 注意:return语句后面就不能再写其他代码了,否则会报错:Unreachable code

20.方法调用内存分析

  • 方法没有被调用的时候,都在方法区中的字节码文件(.class)中存储。
  • 方法被调用的时候,需要进入到栈内存中运行。方法每调用一次就会在栈中有一个入栈动作,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值。
  • 当方法执行结束后,会释放该内存,称为出栈,如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。
  • 栈结构:先进后出,后进先出

21.可变个数形参

当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变个数的形参。

///采用可变个数形参来定义方法,传入多个同一类型变量
public static void test(int a ,String...books);

相当于:
//采用数组形参来定义方法,传入多个同一类型变量
public static void test(int a ,String[] books);

特点:

  1. 可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个

  2. 可变个数形参的方法与同名的方法之间,彼此构成重载

  3. 可变参数方法的使用与方法参数部分使用数组是一致的,二者不能同时声明,否则报错。

  4. 方法的参数部分有可变形参,需要放在形参声明的最后

  5. 在一个方法的形参中,最多只能声明一个可变个数的形参

举例:

eg:n个字符串进行拼接,每一个字符串之间使用某字符进行分割,如果没有传入字符串,那么返回空字符串"":
public class StringTools {
    String concat(char seperator, String... args){
        String str = "";
        for (int i = 0; i < args.length; i++) {
            if(i==0){
                str += args[i];
            }else{
                str += seperator + args[i];
            }
        }
        return str;
    }
}

22.值传递机制

Java里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。

  • 形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参

  • 形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参

23.形参与实参

  • 形参(formal parameter):在定义方法时,方法名后面括号()中声明的变量称为形式参数,简称形参。
  • 实参(actual parameter):在调用方法时,方法名后面括号()中的使用的值/变量/表达式称为实际参数,简称实参。

24.冒泡排序

不断交换相邻元素,将最大或者最小的元素冒泡到序列一端
外层循环:循环排序控制的轮数
内层循环:这一层循环负责每一轮的具体排序过程,也就是不断地比较和交换相邻的元素
  //冒泡排序,实现数组从小到大排序
    public void sort(int[] arr){
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - i - 1; j++) {
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }

25.堆栈

  • 栈:每个方法在调用时,都会有以栈帧的方法压入栈中。栈帧中保存了当前方法中声明的变量:方法内声明的,形参
  • 堆:存放new出来的"东西":对象(成员变量在对象中)、数组实体(数组元素)。

26.递归

分类:直接递归、间接递归。

  • 直接递归:方法自身调用自己。

  • 间接递归:可以理解为A()方法调用B()方法,B()方法调用C()方法,C()方法调用A()方法。

  1. 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。

  2. 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。考虑使用循环迭代

27.MVC设计模式

MVC是一种软件构件模式,目的是为了降低程序开发中代码业务的耦合度。

MVC设计模式将整个程序分为三个层次:视图模型(Viewer)层控制器(Controller)层,与数据模型(Model)层。这种将程序输入输出、数据处理,以及数据的展示分离开来的设计模式使程序结构变的灵活而且清晰,同时也描述了程序各个对象间的通信方式,降低了程序的耦合性。

视图层viewer:显示数据,为用户提供使用界面,与用户直接进行交互。

控制层controller:解析用户请求,处理业务逻辑,给予用户响应
    
模型层model:主要承载数据、处理数据

28.JDK中主要的包介绍

java.lang----包含一些Java语言的核心类,如String、Math、Integer、 System和Thread,提供常用功能
java.net----包含执行与网络相关的操作的类和接口。
java.io ----包含能提供多种输入/输出功能的类。
java.util----包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。
java.text----包含了一些java格式化相关的类
java.sql----包含了java进行JDBC数据库编程的相关类/接口
java.awt----包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面

29.封装

权限修饰符:publicprotected缺省private

体现:

①:成员变量/属性私有化

概述:私有化类的成员变量,提供公共的get和set方法,对外暴露获取和修改属性的功能。

成员变量封装的好处:

  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。
  • 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改。

②:私有化方法

  • 将构造方法私有化,外部类是无法直接进实例化的(类的内部实例化对象,然后拿到外部类用(也就是将此方法封装起来,然后提供一个供外部类访问的方法(单例))

  • public class Singleton {
     
        private static Singleton singleton = new Singleton();
     
        public static Singleton getSingleton() {
            return singleton;
        }
    
    }
    定义一个静态方法,通过类名.方法名,方可实例化,但是通过这种方法,不管声明了多少对象,最终只实例化了一次,也就是都用了一个对象的引用,
    单例设计模式的核心就是封装构造方法,在类的内部实例化对象,然后提供一个静态方法返回实例化对象的引用供外界访问。
    

30.this

  • this可以调用的结构:成员变量、方法和构造器
  • this的理解:this可以指代当前对象(在方法调用时),或者是当前正在创建的对象(在构造器中调用时)

使用格式:

  • this.成员变量:表示当前对象的某个成员变量,而不是局部变量
  • this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
  • this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错

31.super

在Java类中使用super来调用父类中的指定操作:

  • super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
  • super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
  • super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错

注意:

  • 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
  • super的追溯不仅限于直接父类
  • super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

32.子类中调用父类中同名的成员变量

  • 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
  • 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
  • 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问

33.子类构造器中调用父类构造器

① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器。

② 在构造器的首行,“this(形参列表)” 和 "super(形参列表)"只能二选一。

③ 如果在子类构造器的首行既没有显示调用"this(形参列表)“,也没有显式调用"super(形参列表)”,
​ 则子类此构造器默认调用"super()",即调用父类中空参的构造器。

⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。
只能是这两种情况之一。

⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)“,则剩下的那个一定使用"super(形参列表)”。

⑦因为子类必然调用父类的构造器,我们才会将父类中声明的属性或者方法加载到内存中,供子类对象使用

注意:

如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则`编译出错`

34.子类对象实例化全过程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

35.多态

什么是多态:若编译时类型和运行时类型不一致

多态的使用前提:① 类的继承关系 ② 方法的重写

多态的使用:

1、方法内局部变量的赋值体现多态

2、方法的形参声明体现多态

3、方法返回值类型体现多态

4、接口与实现类

1、方法内局部变量的赋值体现多态:
public class TestPet {

    //多态的表现形式
        /*
        编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
        运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
         */
    public static void main(String[] args) {
        //多态引用
        Pet pet = new Dog();
        pet.setNickname("小白");
        pet.eat();//运行时执行子类Dog重写的方法(狗子小白啃骨头)
//      pet.watchHouse();//不能调用Dog子类扩展的方法

        pet = new Cat();
        pet.setNickname("雪球");
        pet.eat();//运行时执行子类Cat重写的方法(猫咪雪球吃鱼仔)
    }
}
2、方法内局部变量的赋值体现多态:
public class Person {
    private Pet pet;

    public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
        this.pet = pet;
    }
    public void feed() {
        pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
    }
    public static void main(String[] args) {
        Person person = new Person();

        Dog dog = new Dog();
        dog.setNickname("小白");
        person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
        person.feed();

        Cat cat = new Cat();
        cat.setNickname("雪球");
        person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
        person.feed();
    }
}
3、方法返回值类型体现多态
public class PetShop {
    //返回值类型是父类类型,实际返回的是子类对象
    public Pet sale(String type){
        switch (type){
            case "Dog":
                return new Dog();
            case "Cat":
                return new Cat();
        }
        return null;
    }
}
public class TestPetShop {
    public static void main(String[] args) {
        PetShop shop = new PetShop();

        Pet dog = shop.sale("Dog");
        dog.setNickname("小白");
        dog.eat();

        Pet cat = shop.sale("Cat");
        cat.setNickname("雪球");
        cat.eat();
    }
}

36.多态的好处和弊端

好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。

弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。(加载了子类的属性和方法但是无法直接访问子类特有的属性和方法(需要向下转型才可))

开发中:
使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。

【开闭原则OCP】

- 对扩展开放,对修改关闭
- 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能

36.向上转型与向下转型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型

    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了

    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。

    • 此时,一定是安全的,而且也是自动完成的

  • 向下转型:(将父类转化为子类,比如得到的类型是Object,需要将其转化为user才可以使用)

  • 当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型

    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断

how:

  • 向上转型:自动完成 ----->多态

  • 向下转型:(子类类型)父类变量

37.instanceof关键字

使用:

1,建议在向上转型(子类转父类)之前,使用instanceof进行判断,避免出现类型转换异常:(ClassCastException)

2,格式:a instanceof A,判断对象a是否是A的实例

3,如果a instanceof A,返回true.则

​ a instanceof superA 返回值也必定是true

37.单例设计模式

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。

核心:静态属性(唯一性),构造器私有,提供访问方法,线程安全

饿汉式:

  • 特点:立即加载,即在使用类的时候已经将对象创建完毕。
  • 优点:实现起来简单;没有多线程安全问题。
  • 缺点:当类被加载的时候,会初始化static的实例,静态变量被创建并分配内存空间,从这以后,这个static的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,并释放所占有的内存。因此在某些特定条件下会耗费内存

懒汉式:

  • 特点:延迟加载,即在调用静态方法时实例才被创建。

  • 优点:实现起来比较简单;当类被加载的时候,static的实例未被创建并分配内存空间,当静态方法第一次被调用时,初始化实例变量,并分配内存,因此在某些特定条件下会节约内存

  • 使用volatile关键字:①:防止指令重排序使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换) ②:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

    
    public class Singleton {
        private static volatile Singleton singleton;
        
        private Singleton(){}
        
        public static Singleton getInstance() {
            if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
                synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
                    if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
        
    }
    

38.main方法

由于JVM需要调用类的main()方法,所以该方法的访问权限必须是public,又因为JVM在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。

39.非静态代码块

作用:

如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。

特点:

  1. 可以有输出语句。
  2. 可以对类的属性、类的声明进行初始化操作。
  3. 除了调用非静态的结构外,还可以调用静态的变量或方法。
  4. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
  5. 每次创建对象的时候,都会执行一次。且先于构造器执行。

39.非静态实例变量赋值顺序

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

40.final

  • 修饰类:类不能被继承,没有子类。提高安全性,提高程序的可读性

  • 修饰方法:表示这个方法不能被子类重写

  • 修饰变量(成员变量或局部变量):一旦赋值,它的值就不能被修改,即常量,常量名建议使用大写字母。

    注意:当定义为基本数据类型的数据和String的时候,只要赋值的时候右边是常量表达式,就会进行编译器的优化.只要不会被编译器内联优化的final属性就可以通过反射进行有效的修改

41.抽象类

  1. 抽象类不能创建对象,只能创建其非抽象子类的对象。

  2. 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。

    理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法(用abstract修饰,但是不写抽象方法),但是有抽象方法的类必定是抽象类。

    抽象类的主要目的是为了定义子类的公共接口和属性,这些接口和属性可以是具体的,也可以是抽象的。抽象类可以没有抽象方法,只包含具体的属性和方法,这样的抽象类仍有其存在的意义,因为它可以定义子类的公共接口和属性。
    
    抽象类可以包含部分实现。在某些情况下,一个类可能需要提供一些通用的实现,但不希望这个类被实例化。这种情况下,这个类可以被声明为抽象类,即使它不包含任何抽象方法。
    
    抽象类可以被用来防止直接实例化。有时候,我们可能希望一个类不能被直接实例化,但又不希望定义任何抽象方法。这种情况下,我们可以将这个类声明为抽象类。
    
  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

42.接口

接口就是规范,定义的是一组规则。

  • 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
  • 声明接口用interface,接口的成员声明有限制:
    • (1)公共的静态常量
    • (2)公共的抽象方法
    • (3)公共的默认方法(JDK8.0 及以上)
    • (4)公共的静态方法(JDK8.0 及以上)
    • (5)私有方法(JDK9.0 及以上)
  • 类可以实现接口,关键字是implements,而且支持多实现。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后。
  • 接口可以继承接口,关键字是extends,而且支持多继承
  • 接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。
  • 接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。

除此之外,接口中没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。

43.使用接口的非静态方法

  • 对于接口的静态方法,只能直接使用“接口名.”进行调用即可
  • 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用

44.内部类

将一个类A定义在另一个类B里面,里面的那个类A就称为内部类(InnerClass),类B则称为外部类(OuterClass)

当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类。总的来说,遵循高内聚、低耦合的面向对象开发原则。

45.基本数据类型、包装类与字符串(String)间的转换

(1)基本数据类型转为字符串

  • 方式1:调用字符串重载的valueOf()方法
  • 方法2:int a = 10;String str = String.valueOf(a);

(2)字符串转为基本数据类型

  • 除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型

    public static int parseInt(String s)
    
  • 字符串转为包装类,然后可以自动拆箱为基本数据类型

    public static Integer valueOf(String s)
    
  • 通过包装类的构造器实现

    * int i = new Integer(“12”);
    

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

46.常用接口

①:list:
List是一个有序的集合,可以包含重复元素。它定义了一系列操作列表的方法,如添加、删除、查找等。ArrayList和LinkedList都是List接口的实现类。
②:set:
Set是一个无序的集合,不允许包含重复元素。它定义了一系列对集合进行操作的方法,如添加、删除、判断元素是否存在等。HashSet和TreeSet都是Set接口的实现类。
③:map:
Map是一种键值对的集合,每个键都是唯一的。它定义了一系列操作Map的方法,如添加、删除、获取值等。HashMap和TreeMap都是Map接口的实现类。
④:Runnable:
Runnable是一个函数式接口,定义了一个可以在新线程中运行的任务。它只有一个无参数无返回值的run()方法,用于执行任务的逻辑。
        //创建自定义类对象(实现Runable接口)  线程任务对象
        MyRunnable mr = new MyR unnable();
        //创建线程对象
        Thread t = new Thread(mr, "长江");
        t.start();
⑤:Comparable:
Comparable是一个需要定义在元素类内部的接口,用于比较对象的大小关系。实现了Comparable接口的类可以通过重写compareTo()方法来定义自己的比较规则。

47.String常用方法

--常用方法:
(1)boolean isEmpty():字符串是否为空
(2)int length():返回字符串的长度
(3)String concat(xx):拼接
(4)boolean equals(Object obj):比较字符串是否相等,区分大小写
(5)boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写
(6)int compareTo(String other):比较字符串大小,区分大小写,按照Unicode编码值比较大小
(7)int compareToIgnoreCase(String other):比较字符串大小,不区分大小写
(8)String toLowerCase():将字符串中大写字母转为小写
(9)String toUpperCase():将字符串中小写字母转为大写
(10)String trim():去掉字符串前后空白符
(11)public String intern():结果在常量池中共享

--查找:
(11)boolean contains(xx):是否包含xx
(12)int indexOf(xx):从前往后找当前字符串中xx,即如果有返回第一次出现的下标,要是没有返回-1
(13)int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
(14)int lastIndexOf(xx):从后往前找当前字符串中xx,即如果有返回最后一次出现的下标,要是没有返回-1
(15)int lastIndexOf(String str, int fromIndex):
返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。  

--字符串截取:
(16)String substring(int beginIndex) :返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。 
(17)String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。(左闭右开) 

--和字符/字符数组相关
(18)char charAt(index):返回[index]位置的字符
(19)char[] toCharArray(): 将此字符串转换为一个新的字符数组返回
(20)static String valueOf(char[] data)  :返回指定数组中表示该字符序列的 String(得到一个含有数组全部元素的String)
(21)static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String
(22)static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String(得到一个含有数组全部元素的String)
(23)static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String

--开头与结尾
(24)boolean startsWith(xx):测试此字符串是否以指定的前缀开始 
(25)boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
(26)boolean endsWith(xx):测试此字符串是否以指定的后缀结束 

--替换
(27)String replace(char oldChar, char newChar):
返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正则。
(28)String replace(CharSequence target, CharSequence replacement):
使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。 
(29)String replaceAll(String regex, String replacement):
使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 
(30)String replaceFirst(String regex, String replacement):
使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 

48.死锁诱因

- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待

以上4个条件,同时出现就会触发死锁。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

49.string内存结构分配

 ①:String s:未初始化
 ②:String s=null;初始化为null
 ③:String s="":初始化为空字符串,调用常量池中的""
 ④:String s4=new String():在堆空间开辟新空间,且调用常量池创造空数组
 ⑤:String s5=new String(""):在堆空间开辟新空间,且调用常量池创造空数组
 注:s4.equals(s5)//true;                s4==s5//false
 String s="abc":常量池创造数组,内容:abc
 String s=new String("abc"):开辟新堆空间,常量池创造数组,内容:abc
 char[] arr={a,b}:字符串常量池创造数组
 String s=new String(arr);开辟新堆空间,常量池创造数组

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

50.new string

String str1 = “abc”; 与 String str2 = new String(“abc”);的区别?

String str2 = new String(“abc”);
总共创建了两个对象,一个是new的对象。另一个是在字符串常量池中生成的字面量
但是:
如果在new之前,写了String str1 = “abc”;
则只用new对象即可(“abc”的字面量已经生成了(String str1 = “abc”;))
  • String s1 = “a”;

说明:在字符串常量池中创建了一个字面量为"a"的字符串。

  • s1 = s1 + “b”;

说明:实际上原来的“a”字符串对象已经丢弃了,现在在堆空间中产生了一个字符串s1+“b”(也就是"ab")。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能。

  • String s2 = “ab”;

说明:直接在字符串常量池中创建一个字面量为"ab"的字符串。

  • String s3 = “a” + “b”;

说明:s3指向字符串常量池中已经创建的"ab"的字符串。

结论:

(1)常量+常量:结果是常量池。且常量池中不会存在相同内容的常量。

(2)常量与变量 或 变量与变量:结果在堆中

(3)拼接后调用intern方法:返回值在常量池中

51.String与其他结构间的转换

字符串 --> 基本数据类型、包装类:

使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型
String s10="1234";
Integer.parseInt(s10);

基本数据类型、包装类 --> 字符串:

相应的valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换。
int t=10;
String st = String.valueOf(t);

字符数组 --> 字符串:

  • String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。
char[] arrChar = {'a', 'b','c'};
String s10 = new String(arrChar,0,2);

字符串 --> 字符数组:

public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法

52.比较器

自然排序:java.lang.Comparable,Comparable 的类必须实现 `compareTo(Object obj) `方法
实现Comparable接口的对象列表(和数组)可以通过 Collections.sort 或 Arrays.sort进行自动排序。

定制排序:java.util.Comparator,重写compare(Object o1,Object o2)方法,比较o1和o2的大小:
可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。

53、反射

反射允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性

框架底层是反射:根据生成的字节码文件在运行时动态的获取类的信息并进行动态操作(创建对象,调用对象),而不需要停止正在运行的代码

54.JDK JRE JVM

jdk包含jre;jre包含jvm

JDK 是 Java 开发工具包,包含了 Java 编译器、Java 虚拟机(JVM)、Java 库和各种开发工具,可以用来开发和编译 Java 应用程序。JDK 包含了 JRE,因此如果只需要运行 Java 程序,只需要安装 JRE 即可。

JRE 是 Java 运行环境,用于运行已经编译好的 Java 应用程序。JRE 包含了 JVM 和 Java 类库,但不包含开发工具。

JVM 是 Java 虚拟机,是 Java 平台的核心组成部分之一。它是一个抽象的计算机,可以在不同的硬件和操作系统上运行 Java 程序,并提供内存管理、垃圾回收等功能。JVM 可以解释 Java 代码或将其编译为本机代码执行。

55.类加载(双亲委派)

步骤:

加载(Loading):加载是指将类的字节码文件加载到内存中。当程序需要使用某个类时,JVM 会通过类的全限定名来查找并加载类的字节码文件。类的字节码文件可以来自本地磁盘、网络或其他来源。加载过程由类加载器(ClassLoader)完成。

连接(Linking):连接是指将已经加载的类与其他类或资源进行关联和准备工作。连接过程包括三个阶段:
①:验证(Verification):验证字节码文件的合法性,确保字节码符合 JVM 规范,不会造成安全问题。
②:准备(Preparation):为类的静态变量分配内存空间,并设置默认初始值。
③:解析(Resolution) :将类的符号引用转换为直接引用,即将类、方法、字段的符号引用解析为实际的内存地址。

初始化(Initialization):初始化是指对类进行初始化操作,包括执行静态变量赋值和静态代码块中的代码以及执行静态方法。初始化过程由 JVM 负责,并且保证在多线程环境下只会执行一次。

机制:

延迟加载:类在首次使用时才会被加载。
双亲委派模型:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
缓存加载:一旦类被加载到内存中,就会被缓存起来,避免重复加载。
为什么使用双亲委派机制:
双亲委派机制可以保证Java核心库的类型安全,因为它不允许用户自己定义一个与Java核心库中的类同名的类(保证类的唯一性)。如果没有双亲委派机制,用户可以在自己的代码中定义一个与Java核心库中的类同名的类,这样会导致Java核心库的类型安全问题。
因此必要情况下也需要打破双亲委派机制
Tomcat服务器和JDBC就打破了双亲委派机制。例如,在Tomcat中,为了解决Web应用程序类之间隔的问题,需要打破双亲委派机制。而JDBC打破了双亲委派机制是为了更好地支持Java的热部署特性。

55.jvm共享区域

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

56.1根据线程共享分类:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

56.1.1程序计数器

①可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
②序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

56.2堆的空间分配

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

堆为什么要划分:

Java堆是垃圾收集器管理的主要区域,因此**很多时候也被称做“GC堆”**。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:**新生代和老年代**。再细致一点的有**Eden空间、From Survivor空间、To Survivor空间**等。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
s0与s1:双方转换时,会将一方执行YGC,然后将剩余数据(以不浪费空间的方式)复制入另一方

56.3JVM创建对象内存分配流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发Young Garbage Collection,即YGC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区。Survivor区分为so和s1两块内存空间。每次YGC的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,就像人到了18岁就会成年一样,在JVM中-XX:MaxTenuringThreshold参数就是来配置一个对象从新生代晋升到老年代的阈值。默认值是15, 可以在Survivor区交换14次之后,晋升至老年代。

56.3.1 逃逸分析

当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸
栈上分配:
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。
同步消除:
如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除。
栈上分配:如果对象不逃逸,可以将其分配在线程的栈上而不是堆上,从而避免了堆内存的分配和垃圾回收开销。
标量替换:如果对象不逃逸,并且其成员变量也不逃逸,可以将对象拆分为独立的标量值,以减少对堆内存的依赖和访问的开销。
同步消除:如果对象不逃逸,可以推断出它不会被多个线程访问,从而可以消除不必要的同步操作。
方法内联:如果对象不逃逸,其方法调用可以被内联展开,减少方法调用的开销。

56.4方法区和永久代以及元空间是什么关系呢?

方法区类似于接口,永久代,元空间类似于接口实现类(1.8前:永久代;1.8后元空间)
方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

56.5元空间(方法区)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

永久代( ≈ 方法区)中用于存放类和方法的元数据以及常量池,比如Class和Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。
①:常量池(Constant Pool):
	常量池是类文件中的一部分,用于存储类或接口中的常量、符号引用和字面量常量。
	常量池中存储了类中使用到的各种字面量值和符号引用,包括字符串常量、基本类型常量、类和接口的全限定名、字段和方法的名称等
②:方法元信息(Method Metadata):
	方法元信息包括了类中定义的所有方法的信息,如方法的访问修饰符、参数列表、返回值类型、异常表等
	方法元信息也作为类的一部分存储在元空间中,它们在类加载时被初始化并在运行时被使用。
③:Class类信息:
	Class类信息指的是描述Java类结构的数据结构,包括类的字段、方法、构造函数等信息
	在元空间中,每个加载的类都会有一个对应的Class实例。
	Class类信息中包含了类的结构信息、访问修饰符、父类、接口信息等,以及类的方法表和字段表等。

元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用--本地内存--。因此,默认情况下,元空间的大小仅受本地内存限制。

56.6静态变量和静态方法的储存

静态变量和静态方法都是属于类级别的,它们在类加载的时候就会被初始化,并且存储在元空间中。
①:静态变量(static variables):
当类被加载到内存中时,其中的静态变量会被初始化并存储在元空间中的静态变量区域。
②:静态方法(static methods):
类的静态方法也会在类加载时被加载到内存中,并存储在元空间中。

56.7类的实例化过程

类被实例化之前,其 Class 类信息会被存储在元空间中,等待类的实例化和运行时的调用。

56.5 Java虚拟机栈

--局部变量表 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

--操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。

--动态链接 每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。

--方法返回地址 方法执行时有两种退出情况:
正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN等异常退出
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:
返回值压入上层调用栈帧
异常信息抛给能够处理的栈帧
PC 计数器指向方法调用后的下一条指令

56.将科学计数法表示的数值转化为小数

      使用BigDecimal的toPlainString方法
      	String a = "2.3E9";
        BigDecimal db = new BigDecimal(a);
        System.out.println("科学计数:" + db.toString());
        System.out.println("普通计数:" + db.toPlainString());(无装饰string)

57.File类的使用

File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。

  • File对象可以作为参数传递给流的构造器。
  • public File(String pathname) :以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。

关于路径:

  • **绝对路径:**从盘符开始的路径,这是一个完整的路径。
  • **相对路径:**相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
    • IDEA中,main中的文件的相对路径,是相对于"当前工程"
    • IDEA中,单元测试方法中的文件的相对路径,是相对于"当前module"
列出目录的下一级
  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
  • public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
判断功能的方法
  • public boolean exists() :此File表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此File表示的是否为目录。
  • public boolean isFile() :此File表示的是否为文件。
创建、删除功能
  • public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false。
  • public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
  • public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建。
  • public boolean delete() :删除文件或者文件夹

58.IO流的分类

  • 按数据的流向不同分为:输入流输出流

    • 输入流 :把数据从其他设备上读取到内存中的流。
      • 以InputStream、Reader结尾
    • 输出流 :把数据从内存 中写出到其他设备上的流。
      • 以OutputStream、Writer结尾
  • 按操作数据单位的不同分为:字节流(8bit)字符流(16bit)

    • 字节流 :以字节为单位,读写数据的流。
      • 以InputStream、OutputStream结尾
    • 字符流 :以字符为单位,读写数据的流。
      • 以Reader、Writer结尾
  • 根据IO流的角色不同分为:节点流处理流

    • 节点流:直接从数据源或目的地读写数据: FileInputStream、FileOutputStrean、FileReader、FileWriter
    • 处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

59.常用的流

常用的节点流:

  • 文件流: FileInputStream、FileOutputStrean、FileReader、FileWriter
  • 字节/字符数组流: ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter
    • 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)。

常用处理流:

  • 缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
    • 作用:增加缓冲功能,避免频繁读写硬盘,进而提升读写效率。
  • 转换流:InputStreamReader、OutputStreamReader
    • 作用:实现字节流和字符流之间的转换。
  • 对象流:ObjectInputStream、ObjectOutputStream
    • 作用:提供直接读写Java对象功能

60.字符流Reader与Writer

Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件(.txt、.java、.c、.cpp、.py)。不能操作图片,视频等非文本文件。

`java.io.Reader`抽象类是表示用于读取**字符流**的所有类的父类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
`java.io.Writer `抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
`java.io.FileReader `类用于读取字符文件,构造时使用系统默认的字符编码和默认字节缓冲区。
 ---`FileReader(File file)`: 创建一个新的 FileReader ,给定要读取的File对象。   
`java.io.FileWriter `类用于写出字符到文件,构造时使用系统默认的字符编码和默认字节缓冲区。
 ---`FileWriter(File file)`: 创建一个新的 FileWriter,给定要读取的File对象。   



注意:当完成流的操作时,必须调用close()方法,释放系统资源,否则会造成内存泄漏。

61.字节流:InputStream和OutputStream

操作非文本文件

`java.io.InputStream `抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
`java.io.OutputStream `抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法
`java.io.FileInputStream `类是文件输入流,从文件中读取字节。
 ---`FileInputStream(File file)`: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。
 `java.io.FileOutputStream `类是文件输出流,用于将数据写出到文件。
 ---`public FileOutputStream(File file)`:创建文件输出流,写出由指定的 File对象表示的文件。 

62.缓冲流

  • 为了提高数据读写的速度,Java API提供了带缓冲功能的流类:缓冲流。

  • 缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

    • 字节缓冲流BufferedInputStreamBufferedOutputStream
    • 字符缓冲流BufferedReaderBufferedWriter
  • 缓冲流的基本原理:在创建流对象时,内部会创建一个缓冲区数组(缺省使用8192个字节(8Kb)的缓冲区),通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

    // 创建字节缓冲输入流
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("abc.jpg"));
    // 创建字节缓冲输出流
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("abc_copy.jpg"));
    
    // 创建字符缓冲输入流
    BufferedReader br = new BufferedReader(new FileReader("br.txt"));
    // 创建字符缓冲输出流
    BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
    

63.异常

异常分类:编译时异常(Exception)(需要被trycatch处理,否则报错)/运行时异常(RuntimeException)

全局异常处理:

    /**
     * 业务异常
     */
    @ExceptionHandler(ServiceException.class)
    public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) {
        log.error(e.getMessage(), e);
        Integer code = e.getCode();
        return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : 			                           AjaxResult.error(e.getMessage());
    }

二.Java多线程

1.进程和线程的区别,进程间如何通信

程 序:为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象

进 程:系统运行的基本单位,进程在运行过程中都是相互独立,但是线程之间运行可以相互影响。

线 程:独立运行的最小单位,一个进程包含多个线程且它们共享同一进程内的系统资源

进程间通过管道、 共享内存、信号量机制、消息队列通信

1.1.线程池

线程池是一种用于管理和复用线程的技术,它通过维护一定数量的线程,接收并处理多个任务,从而提高线程的利用率和系统的性能。

线程池通常由线程池管理器、工作队列和线程组成。

使用线程池可以更加有效地利用系统资源,避免线程创建和销毁的开销,实现任务的并发处理和调度,并且提供了对线程的管理和监控功能。在多线程编程中,使用线程池可以帮助我们编写高效、稳定的并发程序。

1.线程池七大参数

核心线程数:线程池中的基本线程数量

最大线程数:当阻塞队列满了之后,逐一启动

最大线程的存活时间:当阻塞队列的任务执行完后,最大的线程回收时间

最大线程的存活时间单位

阻塞队列:当核心线程满后,后面来的任务都进入阻塞队列

线程工厂:用于生产线程

任务拒绝策略:阻塞队列满后,拒绝任务,有四种策略(1)抛异常(2)丢弃任务不抛异常(3)打回任务(4)尝试与最老的线程竞争

1.2线程池工作

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.3任务队列

有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。

1.4七种任务队列:

ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。

1.5线程工厂

线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并重写 newThread(Runnable r) 方法。
ThreadFactory threadFactory = new ThreadFactory() {
            private int count = 1;
            @Override
            public Thread newThread(@NotNull Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("线程" + count++);
		       return thread;
            }
            };

1.6拒绝策略

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。 4 种拒绝策略:

AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
CallerRunsPolicy:由调用线程处理该任务。
DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

2.什么是线程上下文切换

当一个线程被剥夺cpu使用权时,切换到另外一个线程执行

3.什么是死锁

死锁指多个线程在执行过程中,因争夺资源造成的一种相互等待的僵局

4.死锁的必要条件

- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待

以上4个条件,同时出现就会触发死锁。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

6.什么是AQS锁?
AQS是一个抽象类,可以用来构造锁和同步类,如ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier。

AQS的原理是,AQS内部有三个核心组件,一个是state代表加锁状态初始值为0,一个是获取到锁的线程,还有一个阻塞队列。当有线程想获取锁时,会以CAS的形式将state变为1,CAS成功后便将加锁线程设为自己。当其他线程来竞争锁时会判断state是不是0,不是0再判断加锁线程是不是自己,不是的话就把自己放入阻塞队列。这个阻塞队列是用双向链表实现的

可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话state+1,释放锁的时候就将state-1。当state减到0的时候就去唤醒阻塞队列的第一个线程。

6.1公平锁和非公平锁

公平锁:公平锁保证了锁的获取按照请求的顺序进行,也就是先来先得的FIFO(First-In-First-Out)顺序。
这种方式可以防止饥饿现象,即某个线程长时间得不到锁。
但是由于需要管理和维护一个有序队列,因此公平锁的实现成本和性能都相对较高。
在Java中,可以通过设置ReentrantLock的构造函数参数为true来创建一个公平锁。

非公平锁:非公平锁则不保证锁的获取是按照请求的顺序进行的。
当一个线程请求锁时,如果锁当前没有被其他线程持有,那么这个线程可以直接获取到锁,无需等待,这可能会使得后请求的线程比先请求的线程更早地获取到锁。
非公平锁的优点是吞吐量大,因为线程获取锁的速度通常会比公平锁快,但是可能会出现饥饿现象。
在Java中,可以通过设置ReentrantLock的构造函数参数为false或者使用无参构造函数来创建一个非公平锁。

6.2悲观锁和乐观锁

悲观锁:悲观锁认为在数据处理过程中总是会发生并发问题,因此在每次读写数据时都会先进行加锁操作,确保在操作数据的过程中不会被其他线程干扰。
这种方式的优点是并发控制比较严格,可以防止数据的脏读、不可重复读和幻读,但是缺点是可能会降低系统的并发性能。
乐观锁:乐观锁则认为在数据处理过程中并发问题并不总是会发生,因此在读写数据时并不会进行加锁操作,而是在更新数据时才会检查数据是否已经被其他线程修改过。
这种方式的优点是可以提高系统的并发性能,但是缺点是可能会增加重试的成本,因为当数据被其他线程修改时,当前线程需要重新读取数据并尝试更新。
乐观锁比较适用于读多写少的情况(多读场景),悲观锁比较适用于写多读少的情况(多写场景)。

悲观锁:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

悲观锁主要分为共享锁和排他锁

  • 共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。

乐观锁:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.为什么AQS使用的双向链表?

因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候用双向链表方便删除掉中间的节点

8.有哪些常见的AQS锁
AQS分为独占锁和共享锁

ReentrantLock(独占锁):可重入,可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS去抢占锁,公平锁会按队列顺序排队

Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。

应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票

CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。

应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行

CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值

应用场景:多线程计算数据,最后合并计算结果的应用场景

9.sleep()和wait()的区别

共同点

wait0,wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态

不同点

1.方法归属不同
●sleep(long)是Thread的静态方法
●而wait0,wait(long)都是Object的成员方法,每个对象都有

2.醒来时机不同
●执行sleep((long)和wait(long)的线程都会在等待相应毫秒后醒来,wait(long)和wait0还可以被notify唤醒,wait0如果不唤醒就一直等下去,
它们都可以被打断唤醒

3.锁特性不同(重点)
●wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
●wit方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)
●而sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)

10.yield()和join()区别

当调用yield()方法时,当前线程会暂停执行并让出CPU资源,以便其他线程有机会运行。

但是,调用yield()方法并不保证当前线程一定会被挂起,因为这取决于操作系统的调度器。

A线程中调用B线程的join() ,则B执行完前A进入阻塞状态

12.Java内存模型

JMM(Java内存模型 )屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量在主存中读取

具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的

由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题

13.保证并发安全的三大特性?

原子性:一次或多次操作在执行期间不被其他线程影响

可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道

有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排

14.volatile

保证变量的可见性和有序性,不保证原子性。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。

单例模式双重校验锁变量为什么使用 volatile 修饰? 禁止 JVM 指令重排序,new Object()分为三个步骤:为实例对象分配内存,用构造器初始化成员变量,将实例对象引用指向分配的内存;实例对象在分配内存后实才不为null。如果分配内存后还未初始化就先将实例对象指向了内存,那么此时最外层的if会判断实例对象已经不等于null就直接将实例对象返回。而此时初始化还没有完成。

15.线程使用方式

(1)继承 Tread 类

(2)实现 Runnable 接口

(3)实现 Callable 接口:带有返回值

(4)线程池创建线程

16.ThreadLocal原理
原理是为每个线程创建变量副本,不同线程之间不可见,保证线程安全。每个线程内部都维护了一个Map,key为threadLocal实例,value为要保存的副本。
但是使用ThreadLocal会存在内存泄露问题,因为key为弱引用,而value为强引用,每次gc时key都会回收,而value不会被回收。所以为了解决内存泄漏问题,可以在每次使用完后删除value或者使用static修饰ThreadLocal,可以随时获取value

17.什么是CAS锁
CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。这样的缺点是

(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作

(2)CAS频繁失败导致CPU开销大

(3)ABS问题:线程1和线程2同时去修改一个变量,将值从A改为B,但线程1突然阻塞,此时线程2将A改为B,然后线程3又将B改成A,此时线程1将A又改为B,这个过程线程2是不知道的,这就是ABA问题,可以通过版本号或时间戳解决

18.Synchronized锁原理和优化
Synchronize是通过对象头的markwordk来表明监视器的,监视器本质是依赖操作系统的互斥锁实现的。操作系统实现线程切换要从用户态切换为核心态,成本很高,此时这种锁叫重量级锁,在JDK1.6以后引入了偏向锁、轻量级锁、重量级锁

偏向锁:当一段代码没有别的线程访问,此时线程去访问会直接获取偏向锁

轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会升级为轻量级锁。线程会通过CAS方式获取锁,不会阻塞,提高性能,

重量级锁:轻量级锁自旋一段时间后线程还没有获取到锁,会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低

注意,锁只能升级不能降级

19.如何根据 CPU 核心数设计线程池线程数量

IO 密集型:线程中十分消耗Io的线程数*2
CPU密集型: cpu线程数量

20.AtomicInteger的使用场景
AtomicInteger是一个提供原子操作的Integer类,使用CAS+volatile实来现线程安全的数值操作。

20.常用方法

* public void run() :此线程要执行的任务在此处定义代码。
* public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
* public String getName() :获取当前线程名称。
* public void setName(String name):设置该线程名称。
* public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread子类中就是this,通常用于主线程和Runnable实现类
* public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
* public static void yield():
           yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
* public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。 
* void join() :等待该线程终止。 (将调用join的线程优先执行,当前正在执行的线程阻塞,直到调用join方法的线程执行完毕
或者被打断,主要用于线程之间的交互。)
  void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。 
  void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。 

21.守护线程

它是在后台运行的,它的任务是为其他线程提供服务的,这种线程被称为“守护线程”。JVM的垃圾回收线程就是典型的守护线程。

就是如果所有非守护线程都死亡,那么守护线程自动死亡。

调用setDaemon(true)方法可将指定线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。

调用isDaemon()可以判断线程是否是守护线程。

22.多线程的生命周期

`NEW(新建)`:线程刚被创建,但是并未启动。还没调用start方法。
`RUNNABLE(可运行)`:这里没有区分就绪和运行状态。因为对于Java对象来说,只能标记为可运行,至于什么时候运行,不是JVM来控制的了,是OS来进行调度的,而且时间非常短暂,因此对于Java对象的状态来说,无法区分。
`Teminated(被终止)`:表明此线程已经结束生命周期,终止运行。
阻塞状态分为三种:`BLOCKED`、`WAITING`、`TIMED_WAITING`。
`BLOCKED(锁阻塞)`:在API中的介绍为:一个正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行机会。
- 比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
`TIMED_WAITING(计时等待)`:在API中的介绍为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。
- 当前线程执行过程中遇到Thread类的`sleep`或`join`,Object类的`wait`,LockSupport类的`park`方法,并且在调用这些方法时,`设置了时间`,那么当前线程会进入TIMED_WAITING,直到时间到,或被中断

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

23.线程安全

原因:局部变量不能共享(局部变量是每次调用方法都是独立的,那么每个线程的run()的ticket是独立的,不是共享数据。)

23.1同步机制解决线程安全的原理

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称它为同步锁。

哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象

23.2synchronized 代码块和同步方法

同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。
格式:

synchronized(同步锁){
     需要同步操作的代码
}

**同步方法:**synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

public synchronized void method(){
    可能会产生线程安全问题的代码
}

对于同步方法来说,同步锁对象只能是默认的:

静态方法:锁对象为类.Class
非静态方法:this

24.synchronized的锁是什么

①:为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

②:同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。

③:对于同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为this或类名.class),但是对于同步方法来说,同步锁对象只能是默认的:

- 静态方法:当前类的Class对象(类名.Class)
- 非静态方法:this

25.高并发情况下保证数据一致性

一:给缓存设置过期时间,是保证最终一致性的终极解决方案。

这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。 

二:采用延时双删策略

- 先淘汰缓存
- 再写数据库
- 休眠1秒,再次淘汰缓存
这个休眠的时间需要评估项目的读数据业务逻辑的耗时,确保请求结束时,写请求可以删除读请求造成的缓存脏数据

public void use(String key,Object data){

        redis.delKey(key);

        db.updateData(data);

        Thread.sleep(800);
        
        redis.delKey(key);
    }

26.并发,并行,串行的区别

并发、并行、串行的区别
串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着

并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。

并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行

27.不使用stop()和suspend()方法

stop()方法会立即终止线程的执行,并且可能导致线程在不可预测的状态下结束,可能会引发资源泄漏或数据不一致的问题。
suspend()方法会暂停线程的执行,但并不会释放线程所占用的锁资源,容易导致死锁问题。

28.synchronized和 java.util.concurrent.locks.Lock的有哪些异同?

  1. 使用方式:
    • synchronized是Java中内置的关键字,可以直接在方法或代码块上使用,无需显式地创建锁对象。
    • Lock是Java中提供的接口,需要显式地创建锁对象,并通过lock()和unlock()方法来获取和释放锁。
  2. 锁类型:
    • synchronized只支持一种锁类型,即独占锁(排它锁)。
    • Lock接口提供了多种锁类型,包括独占锁、共享锁等。
  3. 粒度:
    • synchronized锁的粒度比较粗,只能对整个方法或代码块进行加锁。
    • Lock接口提供的锁可以更细粒度地控制线程的访问,例如可以对某个数据结构中的某个元素进行加锁。
  4. 可重入性:
    • synchronized是可重入的,同一个线程可以多次获取同一个锁,而不会发生死锁。
    • Lock也是可重入的,但需要手动进行释放,否则会发生死锁。
  5. 性能:
    • 在低并发情况下,synchronized的性能优于Lock。
    • 在高并发情况下,Lock的性能优于synchronized。
  6. 等待可中断:
    • synchronized不支持等待可中断的锁获取操作。
    • Lock接口提供了tryLock(long time, TimeUnit unit)方法,可以在指定时间内等待锁的释放,并且支持中断等待。
synchronized和Lock都是Java中用于实现线程同步的机制,它们各有优缺点,应根据具体情况选择使用。在低并发情况下,synchronized的性能更好;在高并发情况下,Lock的性能更好。如果需要更细粒度的控制,或者需要支持等待可中断的锁获取操作,则应该使用Lock。

Redis

20道经典Redis面试题-CSDN博客

1.1跳表 todo

1.Redis 支持哪几种数据类型?

String、List、Set、ZSet、hashes

1.2数据结构:
  • String 类型的底层的数据结构实现主要是 SDS(简单动态字符串

    • SDS 不仅可以保存文本数据,还可以保存二进制数据

    • DS 获取字符串长度的时间复杂度是 O(1)

    • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出

  • List 类型的底层数据结构是由双向链表或压缩列表实现的,但是在 Redis 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表

  • Hash 类型的底层数据结构是由压缩列表或哈希表实现的,在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

  • Set 类型的底层数据结构是由哈希表或整数集合实现的:

  • Zset 类型的底层数据结构是由压缩列表或跳表实现的,在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

1.3应用场景:
  • String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
  • List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
  • Hash 类型:缓存对象、购物车等。
  • Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  • Zset 类型:排序场景,比如排行榜、电话和姓名排序等。

2. redis为什么这么快

Redis 基于内存,内存的访问速度是磁盘的上千倍;
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用
Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3,Redis的过期策略

我们在set key的时候,可以给它设置一个过期时间,比如expire key 60。指定这key60s后过期,60s后,redis是如何处理的嘛?我们先来介绍几种过期策略:

定时过期

每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

惰性过期

只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

定期过期

每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。

Redis中同时使用了惰性过期和定期过期两种过期策略。

假设Redis当前存放30万个key,并且都设置了过期时间,如果你每隔100ms就去检查这全部的key,CPU负载会特别高,最后可能会挂掉。
因此,redis采取的是定期过期,每隔100ms就随机抽取一定数量的key来检查和删除的。
但是呢,最后可能会有很多已经过期的key没被删除。这时候,redis采用惰性删除。在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且已经过期了,此时就会删除。

但是,如果定期删除漏掉了很多过期的key,然后也没走惰性删除。就会有很多过期key积在内存内存,直接会导致内存爆的。或者有些时候,业务量大起来了,redis的key被大量使用,内存直接不够了,在没有加大内存的情况下。难道redis直接这样挂掉?不会的!Redis用8种内存淘汰策略保护自己~

4.Redis 数据淘汰策略?

第一种淘汰策略是 noeviction,它是 Redis 的默认策略。在内存超过阀值后,【Redis 不做任何清理工作】,然后对所有写操作返回错误,但对读请求正常处理。noeviction 适合数据量不大的业务场景,将关键数据存入 Redis 中,将 Redis 当作 DB 来使用。

第二种淘汰策略是 volatile-lru,它对【带过期时间的 key】 采用最近最少访问算法来淘汰。使用这种策略,【Redis 选择空闲时间最久的 key 进行淘汰】。这种策略适合的业务场景是,需要淘汰的key带有过期时间,且有冷热区分,从而可以淘汰最久没有访问的key。

第三种策略是 volatile-lfu,它对【带过期时间的 key】 采用最近【最不经常使用的算法来淘汰】。使用这种策略时,【即使用频率最小的 key】,进行淘汰。这种策略也适合大多数 key 带过期时间且有冷热区分的业务场景。

第四种策略是 volatile-ttl,它是对【带过期时间的 key 】中选择【最早要过期的 key】 进行淘汰。使用这种策略时,即最快就要过期的 key,进行淘汰。这种策略适合,需要淘汰的key带过期时间,且有按时间冷热区分的业务场景。

第五种策略是 volatile-random,它是对【带过期时间的 key】 中【随机选择 key】 进行淘汰如果需要淘汰的key有过期时间,没有明显热点,主要被随机访问,那就适合选择这种淘汰策略。

第六种策略是 allkey-lru,它是对【所有 key】,而非仅仅带过期时间的 key,采用【最近最久没有使用的算法】来淘汰。这种策略与 volatile-lru 类似,都是从随机选择的 key 中,选择最长时间没有被访问的 key 进行淘汰需要对所有 key 进行淘汰,且数据有冷热读写区分的业务场景。

第七种策略是 allkeys-lfu,它也是针对【所有 key】 采用最近【最不经常使用】的算法来淘汰。选择【访问频率最小】的 key 进行淘汰这种策略适合的场景是,需要从所有的 key 中进行淘汰,但数据有冷热区分,且越热的数据访问频率越高。

第八种策略是 allkeys-random,它是针对【所有 key】 进行随机算法进行淘汰。它也是从【随机选择 key】,然后进行删除回收。如果需要从所有的 key 中进行淘汰,并且 key 的访问没有明显热点,被随机访问,即可采用这种策略。

5.Redisson原理

当前开源框架Redisson就解决了分布式锁问题。我们一起来看下Redisson底层原理是怎样的吧:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。

6.MySQL与Redis 如何保证双写一致性

  • 缓存延时双删
  • 删除缓存重试机制
  • 读取biglog异步删除缓存

a.延时双删流程:

  1. 先删除缓存
  2. 再更新数据库
  3. 休眠一会(比如1秒),再次删除缓存。

b.删除缓存重试机制:

因为延时双删可能会存在第二步的删除缓存失败,导致的数据不一致问题。可以使用这个方案优化:删除失败就多删除几次,保证删除缓存成功就可以了 所以可以引入删除缓存重试机制

删除缓存重试流程

  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息,获取要删除的key
  5. 重试删除缓存操作

c.读取biglog异步删除缓存

重试删除缓存机制还可以吧,就是会造成好多业务代码入侵。其实,还可以这样优化:通过数据库的binlog来异步淘汰key。

以mysql为例吧

  • 可以使用阿里的canal将binlog日志采集发送到MQ队列里面
  • 然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

7.为什么Redis 6.0 之后改多线程呢?

redis使用多线程并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。

这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。

8.Redis 事务机制

Redis事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。

Redis执行事务的流程如下:

  • 开始事务(MULTI)
  • 命令入队
  • 执行事务(EXEC)、撤销事务(DISCARD )

9.Redis底层,使用的什么协议?

RESP主要有实现简单、解析速度快、可读性好等优点。

10.布隆过滤器

它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。也可以把布隆过滤器理解为一个set集合,我们可以通过add往里面添加元素,通过contains来判断是否包含某个元素。

11.Redis 的持久化机制

Redis是基于内存的非关系型K-V数据库,既然它是基于内存的,如果Redis服务器挂了,数据就会丢失。为了避免数据丢失了,Redis提供了持久化,即把数据保存到磁盘。

Redis提供了RDB和AOF两种持久化机制;

RDB,就是把内存数据以快照的形式保存到磁盘上。具体是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。

AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。

12.为什么使用缓存

1、高性能
操作缓存就是直接操作内存,所以速度相当快。
2、高并发
MySQL 这类的数据库的 QPS 大概都在 1w 左右,Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+QPS【(Query Per Second):服务器每秒可以执行的查询次数;直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的】

13.什么是热Key问题,如何解决热key问题

什么是热Key呢?在Redis中,我们把访问频率高的key,称为热点key。

如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。

而热点Key是怎么产生的呢?主要原因有两个:

用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
请求分片集中,超过单Redi服务器的性能,比如固定名称key,Hash落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点Key问题。

那么在日常开发中,如何识别到热点key呢?

凭经验判断哪些是热Key;
客户端统计上报;
服务代理层上报

如何解决热key问题?

Redis集群扩容:增加分片副本,均衡读流量;
将热key分散到不同的服务器中;
使用二级缓存,即JVM本地缓存,减少Redis的读请求。

14.Redis 集群如何选择数据库? Redis 集群目前无法做数据库选择,默认在 0 数据库。

15.集群(高可用)

怎么实现Redis的高可用?
我们在项目中使用Redis,肯定不会是单点部署Redis服务的。因为,单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。主从模式

  • 1,主从模式中,Redis部署了多台机器,有主节点,负责读写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制

主从复制包括全量复制增量复制两种。一般当slave(从节点)第一次启动时或者认为是第一次连接连接master(主节点),就采用全量复制

全量同步之后,master(主节点)上的数据,如果再次发生更新,就会触发增量复制

  • 2,哨兵模式
    主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址。显然,多数业务场景都不能接受这种故障处理方式。Redis从2.8开始正式提供了Redis Sentinel(哨兵)架构来解决这个问题。

哨兵模式,由一个或多个Sentinel(哨兵)实例组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点。但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,并且各个哨兵之间还会进行监控。简单来说,哨兵模式就三个作用:

  • 发送命令,等待Redis服务器(包括主服务器和从服务器)返回监控其运行状态;
  • 哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机;
  • 哨兵之间还会相互监控,从而达到高可用。

3,Cluster集群模式
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此,Cluster集群应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能。

Cluster集群节点的通讯

一个Redis集群由多个节点组成,各个节点之间是怎么通信的呢?通过Gossip协议!

特别的,每个节点是通过集群总线(cluster bus) 与其他的节点进行通信的。通讯时,使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是 16379。nodes 之间的通信采用特殊的二进制协议。

Hash Slot插槽算法

既然是分布式存储,Cluster集群使用的分布式算法是一致性Hash嘛?并不是,而是Hash Slot插槽算法。
插槽算法把整个数据库被分为16384个slot(槽),每个进入Redis的键值对,根据key进行散列,分配到这16384插槽中的一个。使用的哈希映射也比较简单,用CRC16算法计算出一个16 位的值,再对16384取模。数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点都可以处理这16384个槽。

16.读写一致性

  • 想要提高应用的性能,可以引入缓存来解决
  • 引入缓存后,需要考虑缓存和数据库一致性问题,可选的方案有:更新数据库 + 更新缓存、更新数据库 + 删除缓存
  • 更新数据库 + 更新缓存方案,在并发情况下无法保证缓存和数据一致性,且存在缓存资源浪费和机器性能浪费的情况发生
  • 在更新数据库 + 删除缓存的方案中,先删除缓存,再更新数据库在并发情况下依旧有数据不一致问题,解决方案是延迟双删,但这个延迟时间很难评估,所以
  • 推荐用先更新数据库,再删除缓存
    在先更新数据库,再删除缓存方案下,为了保证两步都成功执行,需配合消息队列或订阅变更日志的方案来做,本质是通过重试的方式保证数据一致性
解决方案(延迟双删):
  • 先淘汰缓存
  • 再写数据库
  • 休眠1秒,再次淘汰缓存

这个休眠的时间需要评估项目的读数据业务逻辑的耗时,确保请求结束时,写请求可以删除读请求造成的缓存脏数据

17.ThreadLocal

栈中独享空间的,用以存放数据;

18.分布式

慢查询 explain执行计划 chmod

分布式集群中,找哪个节点来完成任务,使用负载均衡,②:集群内部选择使用哪个,也会用到负载均衡
集群(相同功能,多个仓储)和分布(婚庆和仓储)
如果某个节点挂了--->注册中心:每个节点启动都需要在注册中心注册,挂了也会在注册中心显示
配置中心:统一管理配置文件(空间换时间,用空间换取配置配置文件的时间)
服务降级(熔断):下游服务挂了但是不会影响到上游服务
限流(拦截器):

Mybaits

https://blog.csdn.net/u010358168/article/details/118195471?ops_request_misc=&request_id=&biz_id=102&utm_term=mybatis%20plus%20%E4%B8%80%E5%AF%B9%E5%A4%9A%20%E5%A4%9A%E5%AF%B9%E4%B8%80%E6%B3%A8%E8%A7%A3&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-118195471.142v96pc_search_result_base8&spm=1018.2226.3001.4187(mybatis-plus)

1,${}和#{}

${} 和 #{} 都是 MyBatis 中用来替换参数的,它们二者的区别主要体现在:
1、功能不同:${} 是直接替换,而 #{} 是预处理;
2、使用场景不同:普通参数使用 #{},如果传递的是 SQL 命令或 SQL 关键字,需要使用 ${},但在使用前一定要做好安全验证;
3、安全性不同:使用 ${} 存在安全问题,而 #{} 则不存在安全问题。
1,功能不同
${} 是将参数直接替换到 SQL 中:
Preparing:select from userinfo where id=1
Parameters:(无)
#{} 则是使用占位符的方式,用预处理的方式来执行业务:
Preparing:select from userinfo where id=?
Parameters:1(Integer)
2,${} 的问题:
a,sql注入
b,当参数的类型为字符时:因为传递的参数是字符类型的,而在 SQL 的语法中,如果是字符类型需要给值添加单引号,否则就会报错,而 ${} 是直接替换,不会自动添加单引号,所以执行就报错了。而使用 #{} 采用的是占位符预执行的,所以不存在任何问题
3,#{}的问题
当传递的参数是一个 SQL 命令或 SQL 关键字时 #{} 就会出问题了。比如,当我们要根据价格从高到低(倒序)、或从低到高(正序)查询时,
==>执行的sql:
Preparing:select from goods order by price
==Parameters:asc(string)

综上:在不考虑安全性的问题情况下,当传递的是普通参数时,需要使用 #{} 的方式,而当传递的是 SQL 命令或 SQL 关键字时,需要使用 ${} 来对 SQL 中的参数进行直接替换并执行

2,seGeneratedKeys=“true” keyProperty=“”

MyBatis框架中Mapper映射文件(XML文件)中的一个属性配置。
其中,useGeneratedKeys="true"表示在执行插入语句后,自动获取生成的主键值;keyProperty="user.userId"表示将获取的主键值设置到对应的实体类属性中,即用户对象的userId属性。这样可以方便地获取新增数据的主键值,便于进行后续的操作。

注意:  keyProperty中的id名是实体类中的id名称,而不是数据库中的id,

3.mybatis执行流程

  • 读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
  • 构造会话工厂SqlSessionFactory
  • 会话工厂创建SqlSession对像(包含了执行SQL语句的所有方法)
  • 操作数据库的接口,Executor执行器,同时负责查询缓存的维护
  • Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
  • 输入参数映射
  • 输出结果映射

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

mysql

sql调优

分为三个方面

①:表的设计优化

表的设计优化(参考阿里开发手册《嵩山版》)
①比如设置合适的数值(tinyint int bigint),要根据实际情况选择
②比如设置合适的字符串类型(char和varchar)char定长效率高,varchari可变长度,效率稍低

②:sql语句优化

①SELECT语句务必指明字段名称(避免直接使用select*)
②SQL语句要避免造成索引失效的写法
③尽量用union all代替union ,union会多一次过滤,效率低
④避免在where-子句中对字段进行表达式操作(如subString),因为可能会导致索引失效
⑤Join优化能用innerjoin就不用left join right join,如必须使用一定要以小表为驱动,
内连接会对两个表进行优化,优先把小表放到外边,把大表放到里边。left join或right join,不会重新调整顺序

第三条详解:

使用union all如果出现重复字段,都会展示,而union会过滤掉,导致效率降低

select *from t user where id 2
union all union
select *from t user where id 5

③:主从复制,读写分离

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

聚簇索引,非聚簇索引
聚集索引(Clustered Index),又名覆盖索引
* 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据---必须有,而且只有一个
二级索引(Secondary Index)
* 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键---可以存在多个

聚集索引选取规则:
①:如果存在主键,主键索引就是聚集索引。
②:如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
③:如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。

行数据(row):代表一整行数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

回表查询:
通过二级索引找到对应的主键值,到聚集索引中查找整行数据,这个过程就是回表

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如何判断 SQL 是否走了索引?

EXPLAIN 命令是查看查询优化器如何决定执行查询的主要方法,使用 EXPLAIN 只需在查询语句开头增加 EXPLAIN 这个关键字即。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其结果中的几个重要参数:

id
ID 代表执行 select 子句或操作表的顺序,如果包含子查询,则会出现多个 ID。值越大,优先级越高,越先被执行。值相同的按照由上至下的顺序执行。

select_type(查询类型)
查询类型主要用于区别普通查询、联合查询以及子查询等复杂查询。

type

查询扫描情况,最好到最差依次是:system>const>eq_ref>ref>range>index>All,一般情况下至少保证达到 range 级别,最好能达到 ref。

possible_keys
显示可能应用在这张表中的索引,一个或多个。查询到的索引不一定是真正被使用。

key
实际使用的索引,如果为 null 则表示没有使用索引。因此会出现 possible_keys 列有可能被用到的索引,但是 key 列为 null。

key_len
表示索引中使用的字节数,在不损失精确性的情况下长度越短越好。key_len 显示的值为索引字段的最大可能长度,并非实际使用长度。即 key_len 是根据表定义计算而来。

ref
显示索引的哪一列被使用了,如果可能的话是一个常数,哪些列或常量被用于查找索引列上的值。

rows
根据表统计信息及索引选用情况,估算出找到所需的记录所需要读取的行数。

什么是索引
索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构(B+树),这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
索引(index)是帮助MySQL高效获取数据的数据结构(有序)
提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
通过索列对数据进行排序,降低数据排序的成本,降低了CPU的消耗

M小ySQL的InnoDB引擎采用的B+树的数据结构来存储索引
阶数更多,路径更短
磁盘读写代价B+树更低,因为非叶子节点只存储指针,叶子阶段存储数据
B+树便于扫库和区间查询,因为叶子节点是一个双向链表
创建索引的原则
使用索引的表要大(百万级),唯一性,常用,
1).数据量较大,且查询比较频繁的表(重要)
2).常作为查询条件、排序、分组的字段(重要)
3).字段内容区分度高
4).内容较长,使用前缀索引
5).尽量联合索引(重要)
6).要控制索引的数量(重要)(每个索引都需要占⽤用磁盘空间,索引越多,需要的磁盘空间就越大,修改表时,对索引的重构和更新很麻烦。)
7).如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它
8)此外,删除不在使用的索引
索引失效的几种情况?
  • ①:违反最左前缀法则
  • ②:使用范围查询(如BETWEEN … AND ,比较运算符)会导致范围查询右边的列上的索引失效
  • ③:不要在索引列上进行运算操作,索引将失效
  • ④:字符串不加单引号,造成索引失效。(类型转换)
  • ⑤:以%开头的ike模糊查询,索引失效
  • ⑥:在索引列上使用 IS NULL 或 IS NOT NULL操作。
  • ⑦:在索引字段上使用not,<>,!=。
  • ⑧:or 语句前后没有同时使用索引。当 or 左右查询字段只有一个是索引,该索引失效,只有左右查询字段均为索引时,才会生效;
  • ⑨:当 MySQL 觉得全表扫描更快时(数据少)
where语句优化

1.like语句优化
不要在关键字前面加%,这样无法使用索引(type=ALL),会导致全表扫描

2.使用union all 来替代or条件
使用or关键字无法使用索引,会导致全表扫描

SELECT * FROM users WHERE id = 1 OR name = 'John';

这个查询可能需要全表扫描,因为MySQL可能无法同时使用id和name的索引。
现在,我们可以使用UNION ALL来改写这个查询:

SELECT * FROM users WHERE id = 1
UNION ALL
SELECT * FROM users WHERE name = 'John';

3.不要是用不等于符号(!=或<>)
4.不要使用in或not in

  • 如果in的条件是连续的,用between … and来替代in
  • 用exists替代in,用not exists替代not in (not in是最低效的)
  • 用left join 替代 in
  • 不要用is null,用列名=常量来代替
  • 不要在where子句"="左边进行运算
  • 多列索引,最左前缀原则
常见的聚合查询?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

插入数据
  • 批量插入:使用 INSERT INTO … VALUES (…) 语句插入单条数据时,每次插入都会涉及到磁盘 I/O 操作,效率较低。因此,可以将多条数据合并成一条 INSERT INTO … VALUES (…),(…),(…) 语句,一次性插入多条数据,这样可以减少磁盘 I/O 操作,提高插入效率。

  • 禁用索引:在插入大量数据时,如果表上有多个索引,每次插入时都需要更新索引,这会影响插入效率。因此,可以考虑在插入完数据后再创建索引,或者在插入数据时禁用索引,插入完成后再启用索引。

  • 调整缓冲区大小:MySQL 有一个称为“缓冲区”的内存区域,用于缓存数据和索引。如果缓冲区的大小设置得不够大,插入数据时会频繁地进行磁盘 I/O 操作,影响插入效率。因此,可以适当调整缓冲区大小,以提高插入效率。

  • 使用 LOAD DATA INFILE:如果要插入大量数据,可以考虑使用 LOAD DATA INFILE 语句,将数据文件导入到数据库中。这种方式可以避免逐条插入数据的瓶颈,提高插入效率。

  • 还应该避免在插入数据时对表进行过多的操作,例如触发器、外键约束等,这些操作会降低插入效率。

    -- 客户端连接服务端时,加上参数 -–local-infile
    mysql –-local-infile -u root -p
    -- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
    set global local_infile = 1;
    -- 执行load指令将准备好的数据,加载到表结构中
    load data local infile '/root/sql1.log' into table tb_user fields
    terminated by ',' lines terminated by '\n' ;
    
order by优化

原则:

  1. 覆盖索引:通过创建覆盖索引,可以减少对表的访问次数,提高查询性能。覆盖索引是指索引包含了查询所需的所有字段,不需要回表到主键索引或者聚簇索引中获取数据。
  2. 为 ORDER BY 字段创建索引。如果查询中的 ORDER BY 字段有索引,MySQL 可以直接使用索引进行排序,而不需要额外的排序操作。根据具体情况,可以创建单列索引或者多列组合索引。
  3. 分页查询:如果只需要查询结果的部分数据,可以使用 LIMIT 子句进行分页查询。通过限制返回的行数,可以减少排序的数据量,从而提高查询性能。
  4. 排序缓存:MySQL 有一个称为排序缓存(sort buffer)的内存区域,用于存储排序操作的临时数据。可以通过调整 sort_buffer_size 参数来适配排序缓存的大小,以提高排序性能。
  5. 避免排序大字段:对于较大的文本、二进制字段等,排序操作可能会占用大量的内存和磁盘空间。如果不是必须要对这些大字段进行排序,可以尽量避免排序大字段,从而提高查询性能。
三大范式

第一范式(1NF):字段(或属性)是不可分割的最小单元,即不会有重复的列,体现原子性

(比如地址可以分为省市区)

第二范式(2NF):满足 1NF 前提下,存在一个候选码,非主属性全部依赖该候选码,即存在主键,体现唯一性,专业术语则是消除部分函数依赖(存在主键)

第三范式(3NF):满足 2NF 前提下,非主属性必须互不依赖,消除传递依赖(非主键和主键直接关联)

范式和反范式,以及各自优缺点

范式是符合某一种级别的关系模式的集合。构造数据库必须遵循一定的规则。在关系数据库中,这种规则就是范式。

名称 优点 缺点
范式 范式化的表减少了数据冗余,数据表更新操作快、占用存储空间少。 查询时通常需要多表关联查询,更难进行索引优化
反范式 反范式的过程就是通过冗余数据来提高查询性能,可以减少表关联和更好进行索引优化 存在大量冗余数据,并且数据的维护成本更高(业务中通过冗余字段来减少计算或者查询,如:QQ活跃用户,用一个字段来记录)

最左匹配原则?

顾名思义,最左优先,以最左边为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。

如建立 (a,b,c,d) 索引,查询条件 b = 2 是匹配不到索引的,但是如果查询条件是 a = 1 and b = 2 或 a=1 又或 b = 2 and a = 1 就可以,因为优化器会自动调整 a,b 的顺序。

再比如 a = 1 and b = 2 and c > 3 and d = 4,其中 d 是用不到索引的,因为 c 是一个范围查询,它之后的字段会停止匹配。

InnoDB

1)InnoDB 支持事务

2)InnoDB 支持外键

3)InnoDB 支持 B+ Tree 数据结构的索引且 InnoDB 是聚集索引

5)InnoDB 支持表、行(默认)级锁

InnoDB 的行锁是基于索引实现的,而不是物理行记录上。即访问如果没有命中索引,则也无法使用行锁,将要退化为表锁。

6)InnoDB 必须有唯一索引(如主键),如果没有指定,就会自动寻找或生产一个隐藏列 Row_id 来充当默认主键

B+树
1) B+ 树本质是一棵查找树,自然支持范围查询和排序。
2) 在符合某些条件(聚簇索引、覆盖索引等)时候可以只通过索引完成查询,不需要回表。
3) 查询效率比较稳定(固定为 O(log n)),因为每次查询都是从根节点到叶子节点,且为树的高度。
最左前缀匹配
以最左边为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。
如果有一个复合索引包含多个列,那么在查询时只能使用前面的列来进行索引匹配。也就是说,只有用到了索引的最左边一部分列,才能发挥复合索引的效果。
如何定位慢查询
调试阶段:开启慢查询日志,我设置的时间为2秒,一旦sql执行超过2s就会记录到日志中
慢查询优化

如何分析一条sql执行速度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

事务四大特性(ACID)

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
聚合查询
使用聚合函数的查询就是聚合查询。所有的聚合函数(UDAF)都应该支持分组查询,内置的聚合函数有:

1. sum(列名) 求和        
2. max(列名) 最大值       
3. min(列名) 最小值       
4. avg(列名) 平均值       
5. first(列名)   第一条记录     
6. last(列名)    最后一条记录    
7. count(列名)   统计记录数   注意和count`(*)`的区别
事务

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分组函数

特点:输入多行,最终输出一行。

		count 计数
		sum	 求和
		avg	 平均值
		max	 最大值
		min	 最小值
		
注意:分组函数自动忽略NULL,你不需要提前对NULL进行处理。
	 分组函数中count(*)和count(具体字段)有什么区别。
	 分组函数不能够直接使用在where子句中。(分组函数在使用的时候必须先分组之后才能使用。where执行的时候,还没有分组。所以where后面不能出现分组函数。)
分组查询
eg:①:找出每个部门的最高薪资
		select ename,deptno,max(sal) from emp group by deptno;
   ②:找出“每个部门,不同工作岗位”的最高薪资?
   		select dept, job, max(sal) from emp group by dept, job;

使用having可以对分完组之后的数据进一步过滤。having不能单独使用,having不能代替where,having必须和group by联合使用。
eg:①:找出每个部门最高薪资,要求显示最高薪资大于3000的?
	select dept,max(sal) from emp group by dept having max(sal) > 3000;
	优化:
	select deptno,max(sal) from emp where sal > 3000 group by deptno;
	优化策略:where和having,优先选择where,where实在完成不了了,再选择having。
	②:只能使用having:找出每个部门平均薪资,要求显示平均薪资高于2500的。
	select deptno,avg(sal) from emp group by deptno having avg(sal) > 2500;
关键字执行顺序:
select 
		...
	from
		...
	where
		...
	group by
		...
	having
		...
	order by
		...
 执行顺序:
 		1. from
		2. where
		3. group by
		4. having
		5. select
		6. order by
  eg:找出每个岗位的平均薪资,要求显示平均薪资大于1500的,除MANAGER岗位之外,要求按照平均薪资降序排。
  select job, avg(sal) as avgsal from emp where job <> 'MANAGER' group by job having avg(sal) > 1500 order by
  avgsal desc;
并发事务问题

并发事务问题并且如何解决:用隔离级别解决

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制。即锁的作用是解决并发问题。

从锁的粒度划分,可以将锁分为表锁、行锁以及页锁。

  • 行级锁:是锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。

行级锁开销大,加锁慢,且会出现死锁。但锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

  • 表级锁:是粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。

  • 页级锁:是粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折中的页级,一次锁定相邻的一组记录。

开销和加锁时间界于表锁和行锁之间,会出现死锁。锁定粒度界于表锁和行锁之间,并发度一般。

从使用性质划分,可以分为共享锁、排它锁以及更新锁。

  • 共享锁(Share Lock):S 锁,又称读锁,用于所有的只读数据操作。

S 锁并非独占,允许多个并发事务对同一资源加锁,但加 S 锁的同时不允许加 X 锁,即资源不能被修改。S 锁通常读取结束后立即释放,无需等待事务结束。

  • 排他锁(Exclusive Lock):X 锁,又称写锁,表示对数据进行写操作。

X 锁仅允许一个事务对同一资源加锁,且直到事务结束才释放,其他任何事务必须等到 X 锁被释放才能对该页进行访问。

使用 select * from table_name for update; 语句产生 X 锁。

  • 更新锁:U 锁,用来预定要对资源施加 X 锁,允许其他事务读,但不允许再施加 U 锁或 X 锁。

当被读取的页将要被更新时,则升级为 X 锁,U 锁一直到事务结束时才能被释放。故 U 锁用来避免使用共享锁造成的死锁现象

从主观上划分,又可以分为乐观锁和悲观锁。

  • 乐观锁(Optimistic Lock):顾名思义,从主观上认定资源是不会被修改的,所以不加锁读取数据,仅当更新时用版本号机制等确认资源是否被修改。

乐观锁适用于多读的应用类型,可以系统提高吞吐量。

  • 悲观锁(Pessimistic Lock):正如其名,具有强烈的独占和排它特性,每次读取数据时都会认为会被其它事务修改,所以每次操作都需要加上锁。

Spring

1、Spring 带来哪些好处?

  • 基于 POJO 的轻量级和最小侵入性编程。
  • DI 机制将对象之间的依赖关系交由框架处理,减低组件间的耦合性。
  • 基于 AOP 技术支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。
  • 对于主流的应用框架提供了集成支持

2、说说 Spring 有哪些模块?

​ Spring Core:基础,提供 IOC 和 DI 能力,可以说 Spring 其他所有的功能都依赖于该类库。

​ Spring Aspects:该模块为集成 AspectJ 提供支持。

​ Spring AOP:提供面向方面的编程实现。

​ Spring JDBC:Java 数据库连接。

​ Spring JMS:Java 消息服务。

​ Spring ORM:用于支持 Hibernate、Mybatis 等 ORM 工具。

​ Spring Web:为创建 Web 应用程序提供支持。

​ Spring Test:提供了对 JUnit 和 TestNG 测试框架的支持。

3、Spring 中使用了哪些设计模式?

​ 工厂模式 包括简单工厂和工厂方法,如通过 BeanFactory 或 ApplicationContext 创建 Bean 对象。

​ 单例模式:Spring 中的 Bean 对象默认就是单例模式。

​ 代理模式:Spring AOP 就是基于代理实现的,包括 JDK 动态代理和 CGlib 技术。

​ 模板方法模式:Spring 中 jdbcTemplate 等以 Template 结尾对数据库操作的类就使用到模板模式。

​ 观察者模式:Spring 事件驱动模型就是观察者模式很经典的应用。

​ 适配器模式:Spring MVC 中,DispatcherServlet 根据请求解析到对应的Handler(也就是我们常说的 Controller) 后,开始由 HandlerAdapter 适配器处理。

​ 装饰者模式:使用 DataSource 在不改动代码情况下切换数据源。

​ 策略模式:Spring 对资源的访问,如 Resource 接口。

4、IOC

## 1、什么是 IOC?

----控制权,生命周期,依赖关系
​		  IOC,Inversion of Control,控制反转,指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系,对于某个具体的对象而言,以前是由自己控制它所引用对象的生命周期;
在IOC中,所有的对象都被 Spring 控制,控制对象生命周期的不再是引用它的对象,而是Spring容器,由 Spring 容器帮我们创建、查找及注入依赖对象,而引用对象只是被动的接受依赖对象,所以这叫控制反转。

​		Spring IOC 可谓是 Spring 的核心,对于 Spring 框架而言,所谓 Ioc 就是由 Spring 来负责控制对象的生命周期和对象		间的关系。正这个控制过程中,需要动态的向某个对象提供它所需要的其他对象,这一点是通过 DI(Dependency Injection,依赖注入)来实现的。

## 2、IOC 的作用或好处?
----低耦合,单例,降低代码量和复杂度

​		对象之间的耦合度或者说依赖程度降低;
		资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例;
		同时降低应用开发的代码量和复杂度,使开发人员更专注业务。

## 3、IOC 的实现原理?
​		Spring 的 IOC 是基于工厂设计模式在加上反射实现。
4.DI
		IoC 的一个重点就是在程序运行时,动态的向某个对象提供它所需要的其他对象,这一点是通过DI(Dependency Injection,依赖注入)来实现的,即应用程序在运行时依赖 IoC 容器来 动态注入对象所需要的外部依赖。

5、Spring的AOP

AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个
可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

应用场景:

记录操作日志
缓存处理
Spring中内置的事务处理

AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。

Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为对象生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

AOP包括以下几个重要的概念和组成部分:
---切面(Aspect):切面是一个模块化单元,它通过横切关注点来定义特定的行为。通常情况下,切面会包含诸如日志记录、性能统计、安全控制等与业务逻辑无关的功能。在AOP中,切面可以根据横切关注点将横切逻辑模块化,然后可以将这些横切逻辑应用到多个对象上。
---连接点(Join Point):连接点是在程序执行过程中能够插入切面的点。这些点可以是方法的调用、异常抛出、变量的修改等。在AOP中,切面可以通过将横切逻辑织入到连接点上来实现其功能。
---通知(Advice):通知是切面的具体行为,它表示在连接点上执行的动作。AOP框架提供了不同类型的通知,包括前置通知(Before advice)、后置通知(After advice)、环绕通知(Around advice)、返回通知(After returning advice)和异常通知(After throwing advice)等。
---切点(Pointcut):切点定义了哪些连接点会被切面的通知所影响。通过指定一组连接点的规则或者表达式,切点决定了切面在哪些地方生效。
---引入(Introduction):引入允许在不修改现有类代码的情况下,添加新方法或属性。

5.1.spring的事务是如何实现的

其本质是通过AOP功能,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

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

5.2.spring事务失效机制

①:异常捕获处理,自己处理了异常,没有抛出:
如果出现异常,事务会回滚,但是如果手动将异常捕获,则事务失效
(原理:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉)
解决:在catch块添加throw new RuntimeException(e)抛出
②:抛出检查异常
(原理:spring默认只会回滚非检查异常)
解决:配置rollbackFor属性:@Transactional(rollbackFor=Exception.class)-->只要右异常就会回滚
③:非public方法导致事务失效
(原理:Spring为方法创建代理、添加事务通知、前提条件都是该方法是public的)
解决:改为public

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

(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象

(2)单例模式:Bean默认为单例模式

(3)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略

(4)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术

(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate

(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller

(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。

(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库

7、 MVC是什么?MVC设计模式的好处有哪些

mvc是一种设计模式。(model)-视图(view)-控制器(controller),三层架构的设计模式。用于实现前端页面的展
现与后端业务数据处理的分离。
mvc设计模式的好处
1.分层设计,实现了业务系统各个组件之间的解耦,有利于业务系统的可扩展性,可维护性。
2.有利于系统的并行开发,提升开发效率。

8、Spring 常用的注解有哪些?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中
的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
@Conntroller:控制器的注解,表示是表现层,不能用用别的注解代替

9.mvc执行流程

①用户发送出请求到前端控制器DispatcherServlet
②DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
③HandlerMapping找到具体的处理器,生成处理器对像及处理器拦截器(如果有),再一起返回给DispatcherServlet。
④DispatcherServleti调用HandlerAdapter(处理器适配器)
⑤HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
⑥r方法上添加了@ResponseBody
⑦通过HttpMessageConverter来返回结果转换为SON并响应

图示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

adaptor:额带普特


图示解释:
处理器映射器(HandlerMapping):根据url值找到对应的方法,内部储存url和方法信息的map结构(key:url路径,value:类名#方法名),根据key查value(handler)
eg:
 @PostMapping("listAll")
 public AjaxResult add(@RequestBody TmAttendance attendance) -->(key:listAll,value:类名#add)
 
 处理器执行链:如果被拦截器拦截,则返回拦截器返回结果和handler封装到处理器拦截器链中
 
 处理器适配器主要功能:①:找到handler并且执行 ②处理handler上参数的转换以及返回的处理;

10.springBoot自动装配原理

主要注解:
@SpringBootConfiguration:该注解与@Configuration注解作用相同,用来声明当前也是一个配置类。
@ComponentScan:组件扫描,默认扫描当前引导类所在包及其子包.
@EnableAutoConfiguration:SpringBoot实现自动化配置的核心注解。

@EnableAutoConfiguration:
其中@EnableAutoConfiguration:是实现自动化配置的核心注解。该注解通过@Import注解导入对应的配置选择器
选择器内部就是读取了该项目和
该项目引用的jar包的的classpath路径META-lNF/spring.factories文件中的所配置的类的全类名。
但不是全部加载,只有引入了依赖,spring才会将META-lNF/spring.factories.文件中的所对应的配置的类加载出来(按需加载)

11.BeanUtils

  • copyProperties(Object dest, Object src)

    将源对象的属性值复制到目标对象中。它会自动匹配同名属性进行赋值,不需要手动一个一个设置属性:
    User srcUser = new User("John", 25);
    User destUser = new User();
    BeanUtils.copyProperties(srcUser,destUser);
    
  • setProperty(Object bean, String name, Object value)

    设置指定对象的属性值,其中 bean 是要操作的对象,name 是属性名,value 是要设置的属性值
    User user = new User();
    BeanUtils.setProperty(user, "name", "John");
    
  • getProperty(Object bean, String name)

    这个方法获取指定对象的属性值,其中 bean 是要操作的对象,name 是属性名
    User user = new User("John", 25);
    String name = BeanUtils.getProperty(user, "name");
    
  • populate(Object bean, Map<String, ? extends Object> properties)

    使用给定的属性集合来设置对象的属性值。其中 bean 是要操作的对象,properties 是一个包含属性名和属性值的 Map 对象
    Map<String, Object> properties = new HashMap<>();
    properties.put("name", "John");
    properties.put("age", 25);
    User user = new User();
    BeanUtils.populate(user, properties);
    

12.Spring bean生命周期

生命周期:Bean创建,初始化赋值,使用和销毁

实例化:当Spring容器接收到创建Bean的请求时,会使用Java反射机制或CGLIB等方式创建一个Bean的实例对象。
设置属性:容器在实例化Bean后,会根据配置文件或注解等方式设置Bean的属性值,包括注入依赖、设置参数等。
Aware接口:在设置完Bean的属性后,容器会调用Bean实现了Aware接口的方法,例如BeanNameAware、ApplicationContextAware等,以便让Bean获取自己的名称、应用上下文等信息。
BeanPostProcessor(后置处理器):在Bean实例初始化之前和之后,容器会调用所有注册的BeanPostProcessor接口实现类的方法,这些方法可以对Bean进行自定义的处理。

初始化:容器在完成Bean的实例化、属性设置和Aware接口调用后,会调用Bean实现了InitializingBean接口的方法、或配置文件中通过init-method属性指定的自定义初始化方法,完成Bean的初始化。

使用:经过初始化后,Bean实例可以被容器使用,例如通过getBean()方法获取Bean实例。

销毁:当Bean实例不再需要时,容器会调用Bean实现了DisposableBean接口的方法、或配置文件中通过destroy-method属性指定的自定义销毁方法,完成Bean的销毁。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

13.前后端数据流

nginx(vue(html+css+js))–>ajax–>nginx–>tomcat–>mvc–>Interceptor–>csm–>sql

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

14.事务传播机制

事务传播机制是指在一个事务方法被另一个事务方法调用时,这两个事务方法之间如何进行事务处理。
Spring框架规定了7种类型的事务传播行为。
PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS:使用当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,就新建一个事务

注解

1,@Param

用法:
①当使用了@Param注解来声明参数的时候,SQL语句取值使用#{},${}取值都可以。
②当不使用@Param注解声明参数的时候,必须使用的是#{}来取参数。使用${}方式取值会报错。
③不使用@Param注解时,参数只能有一个,并且是Javabean。在SQL语句里可以引用JavaBean的属性,而且只能引用JavaBean的属性。
④:SQL语句通过@Param注解中的别名把对象中的属性取出来然后复制
⑤:方法有多个参数,需要 @Param 注解

优点:
①:明确参数名称:使用@Param注解可以明确指定参数的名称,避免参数顺序变化或者重载方法导致的错误。这样可以提高代码的可读性和可维护性。
②:参数与占位符一致:@Param注解可以确保参数名与Mapper.XML文件中的占位符名称一致,避免因为参数名与占位符不一致而引发的错误。
缺点:
①:冗余代码:使用@Param注解会在Mapper接口方法中增加注解的代码,可能会导致代码的冗余。
②:额外的注解:使用@Param注解需要在Mapper接口方法中添加额外的注解,可能会增加代码的复杂性。

Tomcat

Tomcat的两个重要身份

1)http服务器也可以看成一个连接器(Connector)

2)Servlet容器(Container

Socket:

  • 网络上具有唯一标识的IP地址和端口号组合在一起构成唯一能识别的标识符套接字(Socket)。
  • 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。网络通信其实就是Socket间的通信。

核心功能:

  • 处理 socket 连接,负责将网络字节流与 Request 和 Response 对象的转化;

  • 加载和管理 Servlet,以及具体处理 Request 请求

功能:

Tomcat 是一种 Web 服务器和 Servlet 容器,用于处理 HTTP 请求和响应。当客户端通过 HTTP 协议向 Tomcat 发送请求时,Tomcat 会接受该请求并创建一个 Socket 连接与客户端进行通信。

Socket 连接是一种基于 TCP/IP 协议的网络连接,它允许在两个计算机之间传输数据。在 Tomcat 中,Socket 连接被用于接收和发送 HTTP 请求和响应的数据。当 Tomcat 接收到一个 HTTP 请求时,它会将该请求的数据读取到 Socket 输入流中,并将该输入流传递给一个 Request 对象。

Request 对象是一个表示 HTTP 请求的 Java 对象,它包含了请求的所有信息,例如请求的 URL、请求方法、请求头、请求参数等。Tomcat 负责将 Socket 输入流中的字节流转换成 Request 对象,以便后续的处理和响应。

在处理完请求后,Tomcat 会创建一个 Response 对象来表示 HTTP 响应。Response 对象包含了响应的状态码、响应头、响应正文等信息。Tomcat 会将 Response 对象中的数据转换成字节流,并将该字节流写入到 Socket 输出流中,以便向客户端发送响应。

因此,Tomcat 在处理 Socket 连接时,负责将网络字节流与 Request 和 Response 对象之间进行转换,以便实现 HTTP 请求和响应的处理和传输。这个过程中,Tomcat 通过使用 Java 的输入输出流来读取和写入字节流,以实现 Socket 连接的数据传输。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

servlet

在整个Web应用中,Servlet主要负责处理请求、协调调度功能。我们可以把Servlet称为Web应用中的**『控制器』**

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请求处理流程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请求如何定位

我们在浏览器上输入`http://localhost:8080/buxuewushu/add.do`是如何找到对应Servlet进行处理呢?
--请求发送到本机的8080端口,被在那里监听的HTTP/1.1的连接器Connector获得
--连接器Connector将字节流转换为容器所需要的ServletRequest对象给同级Service下的容器模块Engine进行处理
--Engine获得地址http://localhost:8080/buxuewushu/add。匹配他下面的Host主机
--匹配到名为localhost的Host(就算此时请求为具体的ip,没有配置相应的Host,也会交给名为localhost的Host进行处理,因为他是默认的主机)
--Host匹配到路径为/buxuewushu的Context,即在webapp下面找到相应的文件夹
--Context匹配到URL规则为*.do的servlet,对应为某个Servlet类
--调用其doGet或者doPost方法
--Servlet执行完以后将对象返回给Context
--Context返回给Host
--Host返回给Engine
--Engine返回给连接器Connector
--连接器Connector将对象解析为字节流发送给客户端

MVC

概念:

Spring Web MVC (Model View Controller)是基于 Servlet API 构建的原始 Web 框架,从一开始就包含在 Spring Framework 中。正式名称“Spring Web MVC”来自其源模块的名称(spring-webmvc),但它通常被称为“Spring MVC”

spring mvc与mvc

MVC 是一种思想,而 Spring MVC 是对 MVC 思想的具体实现。
Spring MVC 是⼀个实现了 MVC 模式,并继承了 Servlet API 的 Web 框架,当⽤户在浏览器中输⼊了 URL 之后,我们的 Spring MVC 项目就可以感知到用户的请求。

现在绝大部分的 Java 项目都是基于 Spring(或 Spring Boot)的,而 Spring 的核心就是 Spring MVC;也就是说 Spring MVC 是 Spring 框架的核心模块,而 Spring Boot 是 Spring 的脚手架

组成:

DispatcherServlet:前置控制器,负责调度其他组件的执行,可以降低不同组件之间的耦合性,是整个Spring MVC的核心模块
Handler:处理器,完成具体的业务逻辑,相当于Servlet
HandlerMapping:DispatcherServlet是通过 HandlerMapping把请求映射到不同的Handler
HandlerInterceptor:处理器拦截器,是一个接口,如果我们需要进行一些拦截处理,可以通过实现该接口完成
HandlerExecutionChain:处理器执行链,包括两部分内容:Handler和HandlerInterceptor(系统会有一个默认的HandlerInterceptor,如果有额外拦截处理,可以添加拦截器进行设置)
HandlerAdapter:处理器适配器,Handler执行业务方法之前,需要进行一系列的操作包括表单的数据验证、数据类型转换、把表单数据封装到POJO等,这些一系列的操作都是由HandlerAdapter完成,DispatcherServlet通过HandlerAdapter执行不同的Handler
ModelAndView:封装了模型数据和视图信息,作为Handler的处理结果,返回给DispatcherServlet
ViewResolver:视图解析器,DispatcherServlet通过它把逻辑视图解析为物理视图,最终把渲染的结果响应给客户端

工作流程:

客户端请求被DispatcherServlet接收
根据HandlerMapping映射到Handler
生成Handler和HandlerInterceptor
Handler和HandlerInterceptor以HandlerExecutionChain的形式一并返回给DispatcherServlet
DispatcherServlet通过HandlerAdapter调用Handler的方法完成业务逻辑处理
返回一个ModelAndView对象给DispatcherServlet
DispatcherServlet把获取的ModelAndView对象传给ViewResolver视图解析器,把逻辑视图解析成物理视图
ViewResolver返回一个View进行视图渲染(把模型填充到视图中)
DispatcherServlet把渲染后的视图响应给客户端

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

伙伴匹配

redisson介绍


Redisson是一个java操作Redis的客户端,提供了基于Redis的各种分布式数据结构和便捷的操作接口,比如分布式锁
开发者像使用本地集合一样使用Redis,完全感知不到Redis的存在。
关键词:Java Redis客户端,分布式数据网格,实现了很多Java中支持的集合。

在伙伴项目中,我使用Redisson分布式锁
BI:我使用Redisson自带的RateLimiter实现了针对单个用户生成图表操作的限流.

如何使用redis实现分布式session

使用Redis分布式Session->分布式多机场景-登录用户信息一致->简单方便->引入依赖->更改配置文件,不需要额外编码

我使用Redis分布式Session来代替Tomcat本地的Session存储,能够在分布式多机场景下保证获取登录用户信息的一致性。
用Redis实现分布式Session的优点是非常简单方便,只需要引入Redis和springsession-data-redis依赖,然后在配置文件中指定Redis的地址和session的store-type为redis,即可自动生效,不用自己额外编码。

使用hash代替string存储用户信息的好处

hash针对用户的单个数据查询方便,节约资源,而string是将整个json对象

设置为string结构:redisTemplate.opsForValue;     hash结构:redisTemplate.opsForHash
Redis的Hash结构采用key/value键值对的形式存储数据,使用Redis的Hash来存储用户信息后,能够很方便地对用户每个属性进行独立的更新和查询操作,而不是更新和返回整个JSON字符串,性能会更高。
举个例子,你想要获取用户的呢称(就4个字符串),但是用户的简介有100KB的大小。
如果用Hash结构,可以只获取昵称,网络传输的内容大小就很小;
而如果用Stig结构整体存储,网络传输数据时会把所有的用户信息都返回出来,增加传输开销。
此外,相比于直接在Spring Boot中使用String类型存储用户信息,使用Hash结构不用额外存储序列化对象信息,可以一定程度上节省内存。

缓存key设置规则

(系统:模块:方法:其他)
         * 主要目的:避免冲突

合理设置线程池参数

使用ThreadPoolExecutor实现灵活的自定义线程池,

核心线程数,最大线程数,空闲线程存活时间,任务队列,线程工厂,拒绝策略,调整参数

白定义线程池参数如下:
1.核心线程数(corePoolSize):线程池中一直保持活动的线程数。可以使用corePoolSize方法来设置。一般
情况下,可以根据系统的资源情况和任务的特性来设置合适的值。
2.最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。可以使用maximumPoolSize方法来
设置。如果所有线程都处于活动状态,而此时又有新的任务提交,线程池会创建新的线程,直到达到最大线程
数。
3.空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,如果这些线程在一定时间
内没有执行任务,则这些线程会被销毁。可以使用keepAliveTime和TimeUnit方法来设置。
4.任务队列(workQueue):用于存放等待执行的任务的阻塞队列。可以根据任务的特性选择不同类型的队
列,如LinkedBlockingQueue、ArrayBlockingQueue等。默认情况下,使用无界阻塞队列,即
LinkedBlockingQueue,但也可以根据需要设置有界队列.。
5.线程工厂(threadFactory):用于创建线程的工厂。可以通过实现ThreadFactory接口自定义线程的创建逻
辑。
6.拒绝策略(rejectedExecutionHandler):当线程池无法接受新的任务时,会根据设置的拒绝策略进行处
理。常见的拒绝策略有AbortPolicy、DiscardPolicy、DiscardOldestPolicyi和CallerRunsPolicy。

我是根据任务的类型以及消耗资源的情况来调整线程池的参数。
比如针对更消耗CPU资源的计算密集型任务(音视频处理、图像处理、数学计算),
我会将核心线程数设置为和CPU核心数相同,可以让每个线程都能利用好CPU的每个核,而且线程之间不用频繁切换,充分利用系统资源;
针对更消耗网络等IO的IO密集型任务(吃带宽/内存/硬盘的读写资源),我会将核心线程数设置得更大,比如CPU核心数的2倍,能够增加并发度、并且提高CPU的利用率。

为什么需要自定义序列化器

使用Redis缓存高频访问用户信息时使用自定义序列化器,为什么需要自定义序列化器,以及自定义序列化器的实现方式?

由于Spring Boot Data Redis默认使用DK序列化器,会将存储到Redis的键值对转化为字节数组,不利于在
Redis可视化工具中阅读、并且不利于跨语言兼容,所以需要指定序列化器。
所以我通过新建RedisTemplateConfig配置类来创建自定义的RedisTemplate Bean,并且通过  redisTemplate.setKeySerializer((RedisSerializer.stringO)指定了Redis Key的序列化方式。

在项目中是如何实现Redis缓存的?

登录用户信息的存储(用户登录态)、主页推荐用户列表的存储

我的项目中,使用Rédis缓存实现了登录用户信息态的存储、主页推荐用户列表的存储:

具体的实现方式:
·对于登绿用户信息的存储,直接使用spring-session-data-redis依赖开启对Redis分布式Session的支持。
·对于主页用户推荐列表的存储,我使用Spring Data Redis整合Redis,并通过RedisTemplate来操作Redis,根据业务类型设计了缓存key的规则,选用string数据结构来存储推荐用户列表。

使用Redis缓存时,有哪些可能出的常见问题?你又是如何解决的?

缓存雪崩,缓存击穿,缓存穿透

1)缓存击穿:缓存击穿指的是某个热门的缓存键在过期后,同时有大量并发请求到达,导致所有请求都穿透缓存
直接访问数据库,造成数据库压力激增。解决方法包括:
·使用互斥锁来保护缓存访问,只允许一个线程重新生成缓存。
·针对缓存失效时的并发请求使用分布式锁,确保只有一个线程重新生成缓存。
2)缓存雪崩:缓存雪崩指的是大量缓存键在相同时间失效,导致大量请求落到数据库上,造成数据库压力激增。
解决方法包括:
·为缓存键设置不同的失效时间,使失效时间分散。
·使用热点数据预热,提前加载热门数据到缓存。
3)缓存过期问题:缓存中的数据过期后可能会导致数据不一致或数据不可用。解决方法包括:
·设置合理的缓存失效时间,避免缓存数据长时间不更新。
·使用缓存的时候检查数据是否过期,如果过期则重新生成缓存。
4)缓存内存问题:如果缓存数据量很大,可能会导致内存占用过多。解决方法包括:
·设置合理的内存限制,避免缓存数据过多。
·使用LRU(Least Recently Used)策略或淘汰算法来淘汰不常用的缓存数据。
5)缓存数据一致性问题:缓存数据和数据库数据不一致。解决方法包括:
·使用缓存更新策略,当数据库数据发生变化时,及时更新缓存。
·使用双写策略,即同时更新数据库和缓存,确保数据一致性。
6)缓存安全问题:某些敏感数据可能不应该被缓存,如果被缓存可能引发安全问题。解决方法包括:
·避免缓存敏感数据。
·使用加密或其他安全措施来保护缓存数据。

在本项目中,我通过给不同的缓存设置不同的随机过期时间(N+n)来解决缓存雪崩问题。

Spring Scheduler定时任务和分布式锁(todo:分布式)

在解决首页加载过慢的问题中,你使用了Spring Scheduler定时任务和分布式锁,请解释一下定时任务的执行原理和此处分布式锁的作用。

@Scheduled--配置定时任务时间周期--分布式锁定时任务执行的唯一性

项目使用Spring Scheduler实现定时任务,我将每个任务定义为独立的Job类,并且给实际需要定时执行的方
法增加@Scheduled注解来开启定时任务。
在@Scheduled注解中,我使用crontab表达式来定义执行定时任务的时间周期,Spring Scheduler会根据这些定义,在时机到达时开启独立的线程来执行任务。
在分布式场景下,可能有多个服务器实例同时执行同一个定时任务,导致并发问题或重复执行,所以用分布式锁来
保证定时任务执行的唯一性。当定时任务要执行时,先去抢锁,只有抢到锁的服务器实例才会执行定时任务。

使用Redisson分布式锁解决了接口幂等性的问题

接口幂等性->防止用户同时操作或重复提交带来的数据不统一(保证任务不同时执行)

Redisson是一个基于Redis的数据网格,它提供了开箱即用的分布式锁功能,用于解决分布式环境下的并发控制
问题。
比如在项目中,使用Redisson分布式锁保证接口幂等性,防止多个用户同时操作或重复提交带来的数据不一致。

Redisson分布式锁的实现是基于Redis的SETNX命令和Lua脚本,具体的实现原理如下:
1.获取锁:当客户端请求获取锁时,Redisson会向Redis发送一个SETNX命令,尝试将一个特定的键(锁的
标识)设置为一个特定的值(客户端标识),并设置锁的超时时间。
2.争用锁:如果多个客户端同时尝试获取同一个锁,只有一个客户端能够成功设置键的值,其他客户端的
SETNX命令将失败,它们会继续尝试获取锁。
3.锁超时:为了防止某个客户端获取锁后发生异常导致锁永远不会被释放,Redisson设置了锁的超时时间。当
锁的超时时间到达后,Redisson会自动释放锁,允许其他客户瑞获取锁.
4.释放锁:当客户端执行完锁保护的操作后,可以主动释放锁,这将删除锁的标识键,或者锁的自动超时也会导
致锁的释放。
5.锁的可重入性:Redisson支持可重入锁,允许同一客户端多次获取同一个锁,然后多次释放锁。只有所有获
取锁的次数都释放后,锁才会被完全释放:
6.锁的续期:如果一个客户端在持有锁时,锁的超时时间即将到期,Redisson会自动为锁续期,防止锁在操作
过程中被自动释放。

注意:
1.waitTime设置为0,只抢一次,抢不到就放弃
2.注意释放锁要写在finally中
        void testwatchDog () {
            RLock lock= redissonclient.getLock("yupao:precachejob:docache:lock");
            try {
                //只有一个线程能获取到锁
                if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
				//todo实际要执行的方法
                    dosomeThings();
                    system.out.println("getLock:"Thread.currentThread().getId());
                }
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            } finally {
				//只能释放自己的锁
                if (lock.isHeldByCurrentThread()) {
                    system.out.println("unLock:"Thread.currentThread().getId()); lock.unlock();
                    lock.unlock();
                }
            }
        }

看门狗机制

redisson中提供的续期机制
开一个监听线程,如果方法还没执行完,就帮你重置redis锁的过期时间。

原理:
1.监听当前线程,默认过期时间是30秒,每10秒续期一次(补到30秒)
2.如果线程挂掉(注意dbug模式也会被它当成服务器宕机),则不会续期

编辑距离算法

编辑距离算法是一种用于度量两个字符串之间的相似以度或差异性的算法,常用于字符串相似以度比较、拼写检查等场景。

在用户匹配功能中,我使用编辑距离算法来计算用户输入的搜索关键词与已有用户信息的匹配程度,并按照相似度
进行排序,从而实现最相似用户的推荐。

使用:根据编辑举例匹配算法计算和该用户匹配度最高的用户(共同标签最多),共同标签越多排名越高(不取第一个,因为第一个是自己),如果没有则随便推荐几个
优化方案:①过滤掉标签为空的用户,②只查需要的数据(id和tags),③:使用定时任务提前查

BI

流程

后端流程:

调用鱼聪明sdk,得到AI响应结果(图表的js代码和分析结论)

保存图表到数据库


0.1:对于文件校验,操作限流:导入redis,redisson依赖,构建RedissonConfig配置类,用于初始化redissonClient单例对象
	再写限流方法(redissonClient.getRateLimiter),应用到智能分析中
1.1使用easyExcel读取表格,数据转化转化为list,进行过滤处理(去空)用stringBuilder接收,并存入数据库
1.2 对Ai在系统层面进行预设,(第三方AI提供了系统预设能力),提前设置AI的功能,职责,回复格式要求
2.1,构造用户清求(用户消息、数据、图表类型)
2.2调用sdk,得到AI响应结果(图表的js代码),再从从AI响应结果中,取出需要的信息,并将数据(生成数据表的代码,分析结果)传递给前端
3.异步化:线程池或者消息队列(用户点击智能分析页的提交按钮时,先把图表立刻保存到数据库中(作为一个任务)
用户可以在图表管理页面查看所有图表(已生成的、生成中的、生成失败)的信息和状态)
	
	3.1线程池:JUC并发编程包:ThreadPoolExecutor,配置ThreadPoolExecutor核心参数,在智能分析模块开启异步任务
	注意更改执行过程中的状态变化
	缺点:任务在内存中,可能丢失-->持久化;如果系统功能越来越多,长耗时任务越来越多,系统会更加复杂-->应用解耦
	
	3.2消息队列:在多个不同的系统,应用之间实现消息传输和储存;使用:Spring Boot RabbitMQ Starter,导入依赖,在yml中进行配置,创建交换机和队列,生产者,消费者

技术栈

项日技术栈:SSM+Spring Boot、Redis、RabbitMQ、小ySQL、小yBatis-Plus、Hutool工具库。
Spring Boot:用于快速构建基础的后端项日,只需要修改配置文件,就能轻松整合SSM、MySQL、Redis.RabbitMQ等依赖。
Redis:基于内存的高性能键值对存储,在项日中负责分布式Session存储、限流功能的实现。
RabbitMQ:时效性极高的消息队列,在项目中用于将A1GC生成图表这一耗时任务进行异步化和解耦。

如何使用AIGC来生成指定格式的json

我使用了第三方AI助手平台(本质上和OpenAl GPT一样的),首先创建了一个智能BI助手,并且编写了一段
系统预设prompt,全局指定助手的职责、输入内容和回复格式。
在这个过程中,我不断地更改prompt并调试输出效果,最终得到了生成内容比较稳定的Prompt。
创建好助手后,我通过A!助手平台官方的SDK调用该助手完成内容的生成。
值得一提的是,A!生成内容是一个比较耗时的操作,为了提高用户的体验,我使用消息队列将生成过程从同步改造为异步。

如何保证用户上传的数据文件的安全性的?

我在项目中主要采用以下几种方式校验文件:
1.文件类型验证:通过检查文件的扩展名或MME类型实现,比如后缀名必须为XsX.
2.文件大小限制:从MultipartFile对象中获取文件大小信息,限制不能超过1-10MB。
此外,还有一些更严格的校验方式,比如:
1.文件格式校验:防止用户改文件后缀名上传非法格式的文件,可以通过匹配特定格式文件的开头或结尾来校验
内容
2.文件内容安全:防止用户上传一些敏感违规内容,可以使用腾讯云数据万象等第三方内容安全服务自动检测

什么是限流?有哪些常见的限流算法?

介绍限流->限流算法->项目aigc 巨消耗资源->RateLimiter底层是基于令牌桶算法实现

限流是一种用于控制请求或事件流量的策略,防止系统在短时间内接收过多的请求或事件,从而导致系统过载或崩
溃,从而确保系统的稳定性和可用性。
在本项目中,由于AIGC是一个消耗资源和成本的重操作,所以我使用Redisson提供的RateLimiter实现了对单
用户使用AI生成图表功能的限流,从而保护系统。

我了解的限流算法:
1.固定窗口计数限流算法:最简单的限流算法之一,它将清求或事件的到达速率限制在固定的窗口内。例如,每
秒最多允许处理10个请求。这个算法的问题在于它无法平滑处理请求,因为在窗口边界可能会出现瞬间的高
负载。
2.滑动窗口计数限流算法:这种算法改进了固定窗口算法,使用滑动窗口来平滑处理请求。窗口内的清求计数按
照时间的流逝而衰减,从而减少了窗口边界的尖峰负载。
3.令牌桶算法:令牌桶算法使用令牌桶来控制请求速率。令牌以固定的速率被添加到令牌桶中,每个清求需要从
令牌桶中获取一个令牌才能被处理。如果令牌桶中没有足够的令牌,请求将被延迟或拒绝。这种算法平滑控制
了请求速率,并且可以处理突发请求。
4.漏桶算法:漏桶算法以恒定的速率漏水,当清求到达时,会尝试向漏桶中添加清求,如果漏桶已满,则清求被
拒绝。漏桶算法可以平滑请求,但不能处理突发请求。

我在项目中使用的Redisson的RateLimiter底层是基于令牌桶算法实现的。

Redisson的RateLimiter

RateLimiter是Redisson基于Redis实现的分布式限流组件,底层使用令牌桶算法,能够控制某个操作或服务在
一定时间内的请求频率,保护系统不被过多的请求压垮。
在项日中,考虑到AI生成图表是一个耗时且耗费资源的操作,我决定给AI生成图表接口增加限流,具体的策略
是:单个用户每秒内最多执行2次生成图表操作。
具体的实现方式如下:
1)集中管理限流器:创建一个RedisLimiterManager类,集中管理整个项日中所有的限流器,并提供创建限流
器的接口。
2)创建限流器:通过向redissonClient传入指定的key来创建限流器,每个用户对应的key不同,对应的限流
器也不同,从而实现不同用户的独立限流
3)设置限流规则:通过rateLimiter的trySetRate方法制定限流规则,每秒内最多获取2个令牌
4)请求令牌:当用户要执行操作时,会执行对应rateLimiter的tryAcquire方法尝试获取令牌,如果能够获取
到,可以执行后续操作,否则抛出TOO MANY REQUEST异常。

消息队列

消息队列就是将消息放到队列里,用队列做存储消息的介质。消息的发送方称为生产者,消息的接收方称为消费者。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

模型

生产者:producer
消费者:consumer
消息:message
消息队列:queue
生产者和消费者之间实现了解耦,互不影响

分布式消息队列有哪些应用场景?

分布式消息队列的作用是将消息从一个发送者传递给一个或多个接收者,从而实现解耦、异步、可靠的系统通信。
常见的应用场景有:

)应用解耦:解耦系统内的不同模块,使它们能够独立开发、部署和扩展。一个模块可以将消息发送到队列,而
其他模块侧可以异步地接收并处理这些消息,而不需要直接调用模块的八P。
2)异步处理:可以将耗时的任务作为消息放入消息队列中,然后由单个或多个工作线程来处理,从而提高系统性
能和响应速度。
3)流量削峰:当系统面临突发的高峰流量时,分布式消息队列可以通过请求排队的方式实现流量的平滑处理,防
止系统过载。
6)分布式事务:有些分布式消息队列支持分布式事务,可以用于确保消息的可靠传递和处理,同时保持一致性。
7)数据特久化:它可以把消息集中存储到硬盘里,服务器重启就不会丢失

在本项目中,使用RabbitMQ消息队列来对耗时的AI生成任务进行异步化处理,同时可以解耦创建任务和执行
任务模块,在系统负载增大时,可以开启多个任务执行服务,用轮训的方式消费消息、并发处理任务。

异步:①:异步并行处理,减少时间消耗②:某个任务挂掉,不影响整个项目运行,且当其恢复时,仍能处理之前未完成的消息

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

解耦:新增业务时,只需让其作为消费者订阅对应主题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

流量消峰:

RabbitMQ怎样保证消息不丢失

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

解决方案:

①:保证消息到MQ:打开消息确认开关,且设置消息确认的回调函数(回调函数就是消息失败后执行的方法)

②:确保消息路由到正确的队列中:打开路由失败通知的开关,且设置回调函数,执行失败通知

③:确保消息在队列中正确存储:交换器,队列,消息都需要持久化

④:确保消息从队列中正确的投递到消费者:开启手动ACK确认

消息重复

1,发送到MQ出现重复(MQ出问题了,生产者从发了)

2,发送到消费者重复(消费者出问题了,MQ从发了)

解决:使消息接收端的处理是幂等性的

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实现:1,MVCC:(生产者需要提供版本号,不太友好)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

​ 2,去重表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

消息队列作用

流量削峰:主要用于在高并发情况下,业务异步处理,提供高峰期业务处理能力,避免系统瘫痪。

假设系统只能处理1000个请求,但这时突然来了3000个请求,如果不加以限制就会造成系统瘫痪。使用消息队列做缓冲,将多余的请求存放在消息队列中,等系统根据自己处理请求的能力去消息队列去。

应用解耦:主要用于当一个业务需要多个模块共同实现,或者一条消息有多个系统需要对应处理时,只需要主业务完成以后,发送一条MQ,其余模块消费MQ消息,即可实现业务,降低模块之间的耦合。

假设某个服务 A 需要调用服务 B,但是服务 B 突然出现问题,这样会导致服务 A 也会出现问题。如果使用消息队列,当服务 A 执行完成之后,发送一条消息到队列中,服务 B 读取到这条消息,那么它立刻开始进行业务的执行。

异步通信:主业务执行结束后从属业务通过MQ,异步执行,减低业务的响应时间,提高用户体验。

假设有一个业务,要先执行服务 A ,然后服务 A 去调用服务 B ,当服务 B 完成之后,服务 A 调用服务 C,这个业务需要一步步走下去。当使用了消息队列之后,服务 A 完成之后,可以同时执行服务 B 和 服务 C ,这样就减低业务的响应时间,提高用户体验。

为什么使用分布式消息队列来存储任务消息?

考察分布式消息队列的优点。
相比于通过本地内存开启队列来存储任务消息,使用分布式消息队列有如下好处:
1.分布式存储:消息存储在分布式的消息队列中而不是本地,有利于分布式系统的扩展。
2.提高系统可靠性:分布式消息队列通常会保证消息的可靠传递,确保消息不会丢失,未即时处理的任务可以在®
消费者准备好时再次处理。
3.高可扩展性:使用分布式消息队列,可以轻松地添加多个任务消费者,提高系统的并发处理能力。
4.任务重试:分布式消息队列通常支持消息的重试机制。如果某个任务由于某种原因未能成功处理,消息队列可
以重新将其推送给消费者,直到成功为止。
在本项日中,使用了RabbitMQ的消息确认机制,只有接受消息并处理成功的情况下,才会确认消息;否则拒绝
消息并通过死信队列进行降级处理(比如修改任务状态为失败)。通过这种机制,可以确保每一个任务都被系统处
理,不会出现丢失。

RabbitMQ优缺点

在选用RabbitMQ消息队列前,我做过充分的技术选型和调研。发现RabbitMQ不仅简单易用(通过阅读官方
文档就能上手开发),而且其时效性极低(延迟最低,微秒级)。此外,RabbitMQ支持消息确认机制、延迟队
列、死信队列等特性,能够满足业务对于消息可靠性的需求,这是我选择它的原因。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

RabbitMQ有哪几种交换机?

1.Direct Exchange(直连交换机):根据消息的路由键(Routing Key)将消息发送到与之完全匹配的队列
上。通常用于点对点通信。
2.Fanout Exchange(广播交换机):将收到的消息发送到所有与之绑定的队列上,忽略路由键。通常用于广
播消息给多个消费者。
3.Topic Exchange(主题交换机):根据消息的路由键和绑定队列的通配符模式来进行匹配,将消息发送到符
合匹配规则的队列上。通常用于支持灵活的消息路由、相对复杂的业务场景。
4.Headers Exchange(头交换机):根据消息的头部信息来进行匹配,将消息发送到符合匹配规则的队列上。
这种方式用的不多,适用于基于消息头部信息进行路由、相对复杂的业务场景。
我在项目中使用了Direct类型的交换机,因为项日只有1种生产者、1组消费者、1种消息类型,选用Direct交
换机的点对点通信已经能够满足需求,且便于理解。

如何使用RabbitMQ的?请简述消息发送和处理流程。

1)引入spring-boot-starter-amqp依赖,并且在application.yml中编写rabbitmq连接配置
2)编写初始化队列类,创建Direct交换机、队列,以及交换机和队列的绑定
3)创建生产者类:通过Spring Boot整合的RabbitTemplate类来操作MQ,比如调用其convertAndSend方
法,向指定的交换机发送指定路由键的消息,比如发送八!生成任务的id.
4)创建消费者类:通过@RabbitListener注解标识处理消息的方法,并在方法内编写对应的业务逻辑(比如获
取A1生成任务的id、消息确认等)

核心特性

如何保证消息可靠性
①消息过期机制:可以给每条消息指定一个有效期,一段时间内未被消费者处理,就过期了。
②消息确认机制:
			为了保证消息成功被消费(快递成功被取走),rabbitmg提供了消息确认机制,当消费者接收到消息后,比如要
			给一个反馈:
			·ack:消费成功
			·nack:消费失败
			·reject:拒绝
			如果告诉rabbitmq服务器消费成功,服务器才会放心地移除消息。支持配置autoack,会自动执行ack命令,接收到消息立刻就成			 功了。一般情况,建议autoack改为false,根据实际情况,去手动确认。
③死信队列:为了保证消息可靠性,提供一个容错机制来处理失败的消息:死信可以通过死信交换机绑定到死信队列。
④消息持久化

Docker

Docker是一个开源的容器化平台,可以使用容器来虚拟化应用程序和服务。它将应用程序和所需的依赖项打包到称为容器的独立单元中,以便可以在任何环境中快速、可靠地部署和运行。使用Docker,开发人员可以轻松地构建、发布和管理应用程序,而不受底层硬件或操作系统的限制。

docker search(搜索镜像)
docker pull(下载镜像)
docker rmi(删除镜像
docker ps 列出所有运行的容器
docker start 容器id        # 启动容器

前后端联调

—跨域解决:

如何解决:

①:将前后端域名,端口改为相同的

②:让服务器告诉浏览器,允许跨域(返回cross-orgin-allow响应头)

  • 方法一:网关支持:

  • 方法二:后端服务器支持:

    • ①:在controller上添加CrossOrigin注解

      @CrossOrigin(origins = {"前端地址"},allowCredentials = "true")
      
    • ②:添加web全局请求拦截器,coraconfig(cora考尔斯)

      **
       * 跨域配置
       */
      @Configuration
      public class WebMvcConfig implements WebMvcConfigurer {
      // 添加跨域映射
          @Override
          public void addCorsMappings(CorsRegistry registry) {
              //设置允许跨域的路径
              registry.addMapping("/**")
                      //设置允许跨域请求的域名
                      //当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
                      .allowedOrigins("http://localhost:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082",
                              "http://127.0.0.1:8083")
                      //是否允许证书 不再默认开启
                      .allowCredentials(true)
                      //设置允许的方法
                      .allowedMethods("*")
                      //跨域允许时间
                      .maxAge(3600);
          }
      }
      

    数据交互

    • spring boot接收JSON数据

      //    使用@RequestBody注解,可以将请求的JSON、XML或其他格式的数据绑定到方法参数上。
      //    Spring会自动将请求的主体内容转换为方法参数所需的对象类型,且json的key与bean的成员变量名要对应。
      
    • 接收表单数据

      ​ ①:使用 @RequestParam 接收单个表单参数

      @PostMapping("/saveData")
      public ResponseEntity<String> saveData(@RequestParam("name") String name, 
                                             @RequestParam("age") int age) {
          // 使用接收到的参数进行处理
          // ...
          
          return ResponseEntity.ok("Data received successfully");
      }
      

      ​ ②:绑定到对象上,使用@ModelAttribute

      @PostMapping("/saveData")
      public ResponseEntity<String> saveData(@ModelAttribute User user) {
          // 使用接收到的参数进行处理
          // ...
          
          return ResponseEntity.ok("Data received successfully");
      }
      

      无论是使用 @RequestParam 还是绑定到对象上,Spring Boot 都会根据表单参数的名称和类型进行自动转换和绑定。如果类型不匹配,会抛出异常或给出警告。

      • 接收url路径(/tmp?name=mike)

        ①:使用 @RequestParam 接收 URL 参数

        @GetMapping("/tmp")
        public ResponseEntity<String> getData(@RequestParam("name") String name) {
            // 使用接收到的参数进行处理
            // ...
            
            return ResponseEntity.ok("Data received successfully");
        }
        当请求 /tmp?name=mike 时,Spring Boot 将自动将 mike 的值赋给 name 参数。
        需要注意的是,如果 URL 中的参数名称与方法参数名称相同,则注解中的参数名称可以省略
        

        ②:使用 @PathVariable 接收 URL 参数:

        @GetMapping("/user/{name}")
        public ResponseEntity<String> getUser(@PathVariable("name") String name) {
            // 使用接收到的参数进行处理
            // ...
            return ResponseEntity.ok("Data received successfully");
        }
        

      前后端时间传递

      数据库使用datetime类型,实体类使用Date(java.util.Date),

      分为两种情况:

      • 如果json的日期是:yyyy-MM-dd格式,则实体类使用date可以直接接收

      • 如果json的日期是:yyyy-MM-dd HH:mm:ss格式,则实体类不能直接使用date(会出json转换异常),需要在实体类属性上使用@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”)注解,

        eg:
            @Excel(name = "首次打卡时间")
            @ApiModelProperty(value = "首次打卡时间")
            @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
            private Date firstTime;
        
      前端传后端:@DateTimeFormat
      //此时库里datetime类型的数据可以用Date类型直接进行存储
            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
           private Date createTime;
      
       注意:①:这里的Date是在java.util.Date;这个包下面的,不要引用错误了
       	②:前端传过来的时间参数为String类型的数据的时候,就可以用如下的注解方式去接收数据,需要注意的是,注解中
       的样式如果为yyyy-MM-dd HH:mm:ss的话,前端穿的时候必须是这样的格式,如果前端只传yyyy-MM-dd的话,就
       会报异常。
       ③:当实体类为date类型时,再写xml时不用判断是否为空串(‘’),只判断为null就行
      
      后端传前端: @JsonFormat
      @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
           private Date createTime;
      
          @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",locale = "zh", timezone = "GMT+8")
           private Date updateTime;
      
      注意:这里的这个注解是在com.fasterxml.jackson.annotation.JsonFormat;这个包下的。并且时区记得加上,不然时间会少八个小时。
      

      配置文件写法:

      @DateTimeFormat和@JsonFormat需要在每个类中都添加,我们可以直接在配置文件中统一配置

      spring:
        mvc:
          date-format: yyyy-MM-dd HH:mm:ss
          throw-exception-if-no-handler-found: true
        jackson:
          date-format: yyyy-MM-dd HH:mm:ss
          time-zone: GMT+8
          serialization:
            write-dates-as-timestamps: true
      

      但是以上方法不支持时间戳的解析。需要重新实现jackson对Date类型和LocalDateTime类型的解析。

linux

ls	list	查看当前文件夹下的内容
02	pwd	(print work directory)	查看当前所在文件夹
03	cd[目录名]	(changge directory)	切换文件夹
04	touch[文件名]	touch	如果文件不存在,新建文件
05	mkdir[目录名]	make directory	创建目录
06	rm[文件名]	remove	删除指定文件
07	clear	clear	清屏

L或其他格式的数据绑定到方法参数上。
// Spring会自动将请求的主体内容转换为方法参数所需的对象类型,且json的key与bean的成员变量名要对应。
```

  • 接收表单数据

    ​ ①:使用 @RequestParam 接收单个表单参数

    @PostMapping("/saveData")
    public ResponseEntity<String> saveData(@RequestParam("name") String name, 
                                           @RequestParam("age") int age) {
        // 使用接收到的参数进行处理
        // ...
        
        return ResponseEntity.ok("Data received successfully");
    }
    

    ​ ②:绑定到对象上,使用@ModelAttribute

    @PostMapping("/saveData")
    public ResponseEntity<String> saveData(@ModelAttribute User user) {
        // 使用接收到的参数进行处理
        // ...
        
        return ResponseEntity.ok("Data received successfully");
    }
    

    无论是使用 @RequestParam 还是绑定到对象上,Spring Boot 都会根据表单参数的名称和类型进行自动转换和绑定。如果类型不匹配,会抛出异常或给出警告。

    • 接收url路径(/tmp?name=mike)

      ①:使用 @RequestParam 接收 URL 参数

      @GetMapping("/tmp")
      public ResponseEntity<String> getData(@RequestParam("name") String name) {
          // 使用接收到的参数进行处理
          // ...
          
          return ResponseEntity.ok("Data received successfully");
      }
      当请求 /tmp?name=mike 时,Spring Boot 将自动将 mike 的值赋给 name 参数。
      需要注意的是,如果 URL 中的参数名称与方法参数名称相同,则注解中的参数名称可以省略
      

      ②:使用 @PathVariable 接收 URL 参数:

      @GetMapping("/user/{name}")
      public ResponseEntity<String> getUser(@PathVariable("name") String name) {
          // 使用接收到的参数进行处理
          // ...
          return ResponseEntity.ok("Data received successfully");
      }
      

    前后端时间传递

    数据库使用datetime类型,实体类使用Date(java.util.Date),

    分为两种情况:

    • 如果json的日期是:yyyy-MM-dd格式,则实体类使用date可以直接接收

    • 如果json的日期是:yyyy-MM-dd HH:mm:ss格式,则实体类不能直接使用date(会出json转换异常),需要在实体类属性上使用@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”)注解,

      eg:
          @Excel(name = "首次打卡时间")
          @ApiModelProperty(value = "首次打卡时间")
          @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
          private Date firstTime;
      
    前端传后端:@DateTimeFormat
    //此时库里datetime类型的数据可以用Date类型直接进行存储
          @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
         private Date createTime;
    
     注意:①:这里的Date是在java.util.Date;这个包下面的,不要引用错误了
     	②:前端传过来的时间参数为String类型的数据的时候,就可以用如下的注解方式去接收数据,需要注意的是,注解中
     的样式如果为yyyy-MM-dd HH:mm:ss的话,前端穿的时候必须是这样的格式,如果前端只传yyyy-MM-dd的话,就
     会报异常。
     ③:当实体类为date类型时,再写xml时不用判断是否为空串(‘’),只判断为null就行
    
    后端传前端: @JsonFormat
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
         private Date createTime;
    
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",locale = "zh", timezone = "GMT+8")
         private Date updateTime;
    
    注意:这里的这个注解是在com.fasterxml.jackson.annotation.JsonFormat;这个包下的。并且时区记得加上,不然时间会少八个小时。
    

    配置文件写法:

    @DateTimeFormat和@JsonFormat需要在每个类中都添加,我们可以直接在配置文件中统一配置

    spring:
      mvc:
        date-format: yyyy-MM-dd HH:mm:ss
        throw-exception-if-no-handler-found: true
      jackson:
        date-format: yyyy-MM-dd HH:mm:ss
        time-zone: GMT+8
        serialization:
          write-dates-as-timestamps: true
    

    但是以上方法不支持时间戳的解析。需要重新实现jackson对Date类型和LocalDateTime类型的解析。

linux

ls	list	查看当前文件夹下的内容
02	pwd	(print work directory)	查看当前所在文件夹
03	cd[目录名]	(changge directory)	切换文件夹
04	touch[文件名]	touch	如果文件不存在,新建文件
05	mkdir[目录名]	make directory	创建目录
06	rm[文件名]	remove	删除指定文件
07	clear	clear	清屏
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值