Java面试必备2023:涵盖200道高频题目,教你如何答好Java面试

JDK 和 JRE 有什么区别?

JDK(Java Development Kit)和JRE(Java Runtime Environment)是Java编程语言的两个不同的依赖软件包。

JDK是Java开发工具箱,它是一个开发人员用来开发、编译和调试Java应用程序的工具包。JDK包含了JRE以及Java编译器、调试器、JavaDoc等工具。如果你需要开发Java应用程序,那么JDK就是必须安装的。

JRE是Java运行时环境,它是一个用来运行Java程序的包。它包含Java虚拟机和Java类库。如果你只需要运行Java程序,那么JRE就足够了,不需要安装JDK。

总之,JDK和JRE都是Java编程必需的软件包,但它们的功能是不同的,JDK包含JRE和开发工具,而JRE只包含Java虚拟机和Java类库用于运行Java应用程序。

== 和 equals 的区别是什么?

== 是 Java 的运算符,用于比较两个对象是否相等。如果比较的是基本类型,那么 == 比较的是值;如果比较的是对象,那么 == 比较的是对象的引用地址,即两个对象是否指向同一个内存地址,而不是对象的实际内容。

equals 是 Object 类的方法,用于比较两个对象是否相等。equals 方法是在 Object 类中定义的,任何一个类都可以重写该方法,从而实现对象相等的比较规则。默认情况下,equals 方法返回的是两个对象的引用地址是否相等,即与 == 的结果相同。

总结来说,== 比较的是引用地址,而 equals 比较的是对象的内容。在使用时需要注意,如果要比较对象的内容是否相等,应该使用 equals 方法。

final 在 java 中有什么作用?

在 Java 中,final 关键字有以下作用:

  1. final 修饰的类无法被继承,即该类是无法被修改和扩展的。

  2. final 修饰的方法无法被子类覆盖,即该方法是无法被修改的。

  3. final 修饰的变量被赋值后无法再被修改,即该变量是不可变的。

  4. final 修饰的方法参数不可被修改。

  5. final 修饰的方法可以被 JVM 进行优化,提高程序的性能。

总之,final 关键字主要可以用来保证程序的安全性、可维护性和可读性。

java 中的 Math.round(-1.5) 等于多少?

在Java中,Math.round(-1.5)的结果是-1。 四舍五入到最接近的整数,如果要舍入的数字恰好在两个整数中间 ,则舍入为偶数的整数。 因此,-1.5四舍五入为-1。

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

String 不属于基础的数据类型,它是 Java 中的一种类(Class),也可以说是一种对象类型。在 Java 中,基础的数据类型有八种:boolean、byte、char、short、int、long、float 和 double。这些基础类型可以用来存储简单的数据值,例如整数、浮点数和布尔值。String 类型则是用来存储文本字符串的。

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

这两个表达式创建的字符串是相同的内容,但是它们不是完全相同的表达式。

第一个表达式使用了字符串字面量,即直接将字符串内容写在代码中。这种方式会在编译时自动创建一个字符串对象,因为Java中字符串是不可变的,所以这个对象在运行时是不可修改的。

第二个表达式使用了String类的构造函数来创建一个新的字符串对象。这种方式会在运行时显式地创建一个新的字符串对象。

因此,虽然这两个表达式创建的字符串内容是相同的,但它们背后的实现方式是不同的。

如何将字符串反转?

Object obj = new Object(); // 待封装的对象
StringBuilder sb = new StringBuilder();
sb.append(obj); // 将对象转化为字符串并添加到stringBuilder中
sb.reverse(); // 反转字符串



<font color=#1E90FF size=5>String 类的常用方法都有那些?

1. charAt(): 返回字符串中指定位置的字符。
2. length(): 返回字符串的长度。
3. indexOf(): 在字符串中查找指定字符或字符串第一次出现的位置。
4. lastIndexOf(): 在字符串中查找指定字符或字符串最后一次出现的位置。
5. substring(): 返回从指定位置开始到字符串结尾的子字符串。
6. toUpperCase(): 将字符串转换为大写字母。
7. toLowerCase(): 将字符串转换为小写字母。
8. trim(): 返回删除了字符串头尾空格的新字符串。
9. split(): 将字符串分割为一个字符串数组,根据指定的分隔符。
10. replace(): 在字符串中替换指定字符或字符串。
11. equals(): 判断两个字符串是否相等。
12. compareTo(): 将字符串与指定字符串进行比较,返回一个整数。
13. contains(): 判断字符串中是否包含指定的字符或字符串。
14. startsWith(): 判断字符串是否以指定的字符或字符串开头。
15. endsWith(): 判断字符串是否以指定的字符或字符串结尾。

<font color=#1E90FF size=5>new String("a") + new String("b") 会创建几个对象?

对象1:new StringBuilder()

对象2:new String("a")

对象3:常量池中的"a"

对象4:new String("b")

对象5:常量池中的"b"

深入剖析:StringBuilder中的toString():

对象6:new String("ab")

强调一下,toString()的调用,在字符串常量池中,没有生成"ab"

<font color=#1E90FF size=5>普通类和抽象类有哪些区别?

普通类和抽象类的主要区别在于:

1. 是否可以实例化:普通类可以被实例化,而抽象类不能被实例化。

2. 是否可以包含抽象方法:普通类不包含抽象方法,而抽象类可以包含抽象方法。抽象方法是一种没有实现的方法,只有方法声明而没有方法体。

3. 继承关系:普通类和抽象类都可以作为父类被其他类继承。但是抽象类作为父类时,其子类必须实现父类的抽象方法;而普通类作为父类时,则没有这个限制。

4. 设计目的:普通类通常用来表示具体的事物,而抽象类通常用来定义一些共性的行为或属性。

5. 使用场景:普通类适用于描述具体的对象和行为,而抽象类适用于创建一些常用的、通用的类和方法,并为其子类提供方法实现的模板。


<font color=#1E90FF size=5>接口和抽象类有什么区别?

接口和抽象类有以下主要区别:

1. 实现方式不同:接口只提供了抽象方法的定义,没有任何实现,而抽象类可以有抽象方法和实现方法的混合。

2. 多继承支持不同:一个类只能继承一个抽象类,但是可以实现多个接口。这是因为抽象类有构造函数,其子类要用到继承的抽象类的构造函数,多继承时可能有构造函数的冲突问题,而接口没有构造函数,因此不存在这种冲突。

3. 访问权限限制不同:接口中的所有成员默认为public,而抽象类的方法可以是protected、public、或者默认访问修饰符。

4. 参数类型不同:在Java中,接口通常用于定义类型,而抽象类通常用于实现继承关系和代码复用。因此,如果需要定义一种新类型,应该使用接口,如果需要实现一些重复的代码,就应该使用抽象类。

5. 设计目的不同:接口的设计目的是为了让不同的类可以互相之间交互,而抽象类的设计目的则是为了抽象出一组相关的类的共同属性和方法,使得它们之间可以有更好的可复用性和维护性。

<font color=#1E90FF size=5>java 中 IO 流分为几种?

Java中的IO流可分为四种:

1. 字节流:以字节为单位进行数据传输,如InputStream和OutputStream等类。

2. 字符流:以字符为单位进行数据传输,如Reader和Writer等类。

3. 缓冲流:将数据缓存在内存中,减少对物理介质的操作次数,提高效率,如BufferedInputStream和BufferedOutputStream等类。

4. 对象流:用于读写Java对象,如ObjectInputStream和ObjectOutputStream等类。

<font color=#1E90FF size=5>BIO、NIO、AIO 有什么区别?


BIO(Blocking I/O)、NIO(Non-Blocking I/O)、AIO( Asynchronous I/O)是不同的I/O模型,主要区别如下:

1. 阻塞I/O(BIO):当一个线程在读取输入流时,如果当前没有数据可用,则该线程会阻断等待,直到数据到来才会继续执行。

2. 非阻塞I/O(NIO):在读取输入流时,如果没有数据可用,则线程不会被阻塞,而是直接返回,继续执行其他任务。可以通过轮询方式不断的检查数据是否到来,减少了线程阻塞的时间。

3. 异步I/O(AIO):可以实现真正的异步I/O,当进行读/写操作时,不需要等待操作完成就可以继续执行其他操作,当操作完成时,会通过回调函数来通知相应的线程进行处理。相比于非阻塞I/O,可以让系统更充分利用CPU资源,提高I/O效率。

因此,BIO适用于连接数较小的场景,NIO适用于连接数较多的场景,AIO适用于I/O操作时间较长的场景。


<font color=#1E90FF size=5>Files的常用方法都有哪些?


在计算机编程中,文件是一种常用的数据存储方式。对于文件的操作,我们会使用一些常用的方法,下面介绍几种常见的文件方法:

1. open(): 打开文件,以便后续读取或写入文件内容。
2. read(): 读取文件内容,在读取文件之前需要使用open()方法打开文件。
3. write(): 向文件中写入内容,如果文件不存在,则会创建该文件;如果存在,则会将新内容追加到文件末尾。
4. close(): 关闭文件,释放文件句柄等资源。
5. seek(): 移动文件指针到指定位置,方便读取特定位置的数据。
6. flush(): 将输出缓存区的内容立即写入文件。
7. tell(): 获取文件指针当前的位置。

这些文件方法可以帮助我们对文件进行读取、写入、追加、关闭等常见操作。在实际应用中,根据需要可以灵活运用这些方法。


<font color=#1E90FF size=5>什么是反射?

反射是指在程序运行的过程中,动态地获取类的信息并操作对象的能力。Java中的反射机制可以让程序在运行时获取类的结构、成员变量、方法、构造方法等信息,并且可以在运行时创建对象、调用方法、获取或设置成员变量的值等。通过反射机制,我们可以在编写程序时不必预先知道类的具体信息,而是在运行时动态获取所需的信息。反射机制为我们提供了极大的灵活性和可扩展性,但是由于其在运行时进行检查,因此会带来一定的运行效率上的损失。


<font color=#1E90FF size=5>什么是 java 序列化?什么情况下需要序列化?

Java序列化是将Java对象转换为字节序列的过程,以便可以将其存储在文件中,网络中或以其他方式传输。反序列化是将字节序列转换回Java对象的过程。

需要序列化的情况包括:

1. 网络传输:当从网络发送对象时,需要将对象序列化为字节流,以便可以在网络上进行传输。

2. 持久化:将对象存储到磁盘或数据库中时,需要将对象序列化为字节序列。

3. 多线程通信:在多线程环境中,可以使用序列化实现线程之间的通信。

4. 远程方法调用:在远程方法调用中,需要将参数和返回值序列化并传输到远程计算机进行处理。


<font color=#1E90FF size=5>为什么要使用克隆?如何实现对象克隆?深拷贝和浅拷贝区别是什么?

Java中使用克隆的目的是为了创建一个与原始对象具有相同状态的新对象,同时避免对原始对象进行更改。这是在某些情况下非常有用的,例如在并发编程中,为了避免多个线程之间修改同一对象的状态,可以使用克隆来创建一个独立的对象。另外,使用克隆也可以提高对象的创建性能,因为克隆通常比创建一个全新的对象更快。

要实现对象克隆,需要在对象类中实现Cloneable接口,并覆盖Object类的clone()方法。被克隆对象的类必须实现Cloneable接口,否则调用clone()方法将会抛出CloneNotSupportedException异常。

实现克隆通常有两种方式:浅拷贝和深拷贝。浅复制只会复制对象中的基本类型和引用类型变量的值,而不会复制引用类型变量所引用的对象。而深拷贝则会把对象中的所有变量都复制一遍,包括引用类型变量所引用的对象。

浅拷贝和深拷贝的区别在于对象复制的深度。浅拷贝只复制对象的顶层属性,而对于深层次的属性,引用类型变量将指向相同的对象。这意味着,如果将浅复制的对象中的引用类型变量修改为引用另一个对象,那么原始对象中对应的变量也将随之修改。深拷贝则可以避免这个问题,因为它创建了一份新的对象,包括引用类型变量所引用的对象。


<font color=#1E90FF size=5>throw 和 throws 的区别?

throw 和 throws 是Java语言中两个关键词,它们的区别如下:

1. throw 表示抛出异常的动作,通常和 try-catch 语句结合使用,用于在程序运行过程中抛出异常。

2. throws 用于声明方法可能抛出的异常类型,表示此方法可能出现异常,需要对可能的异常进行处理或者将异常交给上层调用者处理。

因此,throw 和 throws 的作用不同,throw 是用来抛出异常,throws 是用来声明方法可能抛出的异常类型。


<font color=#1E90FF size=5>final、finally、finalize 有什么区别?

final 是一个关键字,用于修饰类、方法、变量。用于Class前面则表示该类为最终类,即不能被继承;用于method前面则表示该方法为最终方法,即不能被重写;用于变量前面则表示该变量为常量,值不能被修改。

finally 是一个关键字,用于定义在 try-catch 结构中的代码块。不管 try 或 catch 块中是否抛出异常,finally 块中的代码总会被执行。

finalize 是一个方法,用于对象的垃圾回收前执行清理工作。当对象即将被垃圾回收器回收时,垃圾回收器会先调用对象的 finalize 方法,以便让对象能更好地完成资源回收等清理工作。然而 finalize 方法并不能保证一定会被调用,也不建议在程序中重写该方法。

总体来说,final 用于修饰类、方法、变量;finally 是 try-catch 结构中的一个代码块;finalize 是一个方法,用于对象的垃圾回收前的清理工作。


<font color=#1E90FF size=5>try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

是的,finally 块中的代码无论如何都会执行,即使在 catch 块中使用了 return 语句。即使在 catch 中使用了 return 语句,finally 块中的代码也会在返回之前执行。只有在发生异常之前结束 JVM 才会使 finally 语句块不被执行。


<font color=#1E90FF size=5>常见的异常类有哪些?

Java中常见的异常类包括:

1. NullPointerException 空指针异常

2. IndexOutOfBoundsException 数组越界异常

3. ClassNotFoundException 类未找到异常

4. ArithmeticException 算术异常

5. ArrayIndexOutOfBoundsException 数组下标越界异常

6. IllegalArgumentException 非法参数异常

7. InterruptedException 中断异常

8. IOException 输入输出异常

9. NoSuchMethodException 没有找到方法异常

10. IllegalStateException 非法状态异常

11. SecurityException 安全异常

12. SQLException SQL异常

13. RuntimeException 运行时异常

14. ClassCastException 类型转换异常

15. CloneNotSupportedException 克隆不支持异常

16. IllegalAccessException 非法访问异常

17. NumberFormatException 数字格式异常

18. NoSuchFieldException 没有找到字段异常

19. UnsupportedEncodingException 不支持的编码异常


<font color=#1E90FF size=5>hashcode是什么?有什么作用?

HashCode是Java中Object类的一个方法。它返回对象的哈希值。哈希值可以看作是对象的“数字指纹”,为每个对象产生唯一的标识符。

在哈希表中,哈希码会被用作键(key)。当我们向哈希表中添加键值对时,哈希表会根据键的哈希码将键值对存储在不同位置,这样可以提高查询效率。所以,一个良好的哈希码能够减少哈希冲突的出现,提高哈希表的效率。

除了在哈希表中使用,HashCode还常常用于对象的比较。每个对象的HashCode值是唯一的,所以可以通过HashCode快速比较对象是否相等。

总之,HashCode是Java中一个非常重要的概念,它可以提高数据结构的效率,也可以为对象比较提供便利。

<font color=#1E90FF size=5>java 中操作字符串都有哪些类?它们之间有什么区别?

Java 中常用的操作字符串的类包括:String、StringBuilder、StringBuffer。

String 是不可变字符串类,一旦创建便不能被改变。对于每个字符串操作(如连接、替换等),都会创建一个新的 String 对象。

StringBuilder 和 StringBuffer 都是可变字符串类,可以进行字符串的修改,而不会创建新的对象。StringBuilder 是单线程使用的,但速度比 StringBuffer 更快;StringBuffer 是线程安全的,但速度比 StringBuilder 更慢。

因此,如果需要在单线程环境下进行字符串操作,推荐使用 StringBuilder;如果需要在多线程环境下进行字符串操作,应该使用 StringBuffer。如果不需要修改字符串,使用 String 类即可。


<font color=#1E90FF size=5>java 中都有哪些引用类型?

Java中有如下引用类型:

1. 强引用(Strong reference)
2. 软引用(Soft reference)
3. 弱引用(Weak reference)
4. 虚引用(Phantom reference)

<font color=#1E90FF size=5>在 Java 中,为什么不允许从静态方法中访问非静态变量?

在 Java 中,静态方法是一种属于类级别的方法,可以在不创建实例对象的前提下直接调用,而非静态变量是属于对象级别的变量,只能通过创建实例对象后才能访问。因此,从静态方法中访问非静态变量的话,就需要知道具体的对象实例,而静态方法无法获取到对象实例。因此,Java 不允许从静态方法中直接访问非静态变量,必须通过创建对象实例后才能访问。


<font color=#1E90FF size=5>说说Java Bean的命名规范


Java Bean的命名规范如下:

1. 类名必须是一个名词并且具有首字母大写的驼峰命名风格。

2. 属性必须具有私有的访问控制符,并且具有首字母小写的驼峰命名风格。

3. 属性必须具有getter和setter方法,方法名必须与属性名对应,并且具有首字母大写的驼峰命名风格。

4. getter方法必须以get开头并且不带参数,返回值类型必须与属性类型对应。

5. setter方法必须以set开头,并且只有一个参数,参数类型必须与属性类型对应,返回值类型为void。

例如,一个具有三个属性的Java Bean的命名规范如下:

public class Employee {
private String firstName;
private String lastName;
private int age;

public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
    this.firstName = firstName;
}

public String getLastName() {
    return lastName;
}

public void setLastName(String lastName) {
    this.lastName = lastName;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

}



<font color=#1E90FF size=5>Java Bean 属性命名规范问题分析

Java Bean 是一种 Java class 的规范,用于封装数据或状态,并提供公共的访问方法。Java Bean 属性的命名规范是在 Java Beans 规范中定义的,它必须符合一定的规则才能被认为是一个有效的 Java Bean 属性名。

下面是 Java Bean 属性命名规范的一些问题分析:

1. 属性名应该以小写字母开头,但是有些 Java 开发者会仍旧使用大写字母开头,这样做是不符合 Java Bean 规范的。

2. 属性名应该使用驼峰式命名法,例如:firstName、lastName、dateOfBirth 等等。但是有些 Java 开发者可能会使用下划线连接单词,例如:first_name、last_name、date_of_birth 等等。这种方法不太符合 Java Bean 规范的。

3. 属性名不应该使用关键字作为属性名,例如:public、void、if、else、while 等等。这些单词都是 Java 中的关键字,用作属性名可能会导致编译错误。

4. 属性名应该是有意义的,能够描述该属性的作用、含义或内容。例如:firstName 表示名字的“姓”,而 lastName 表示名字的“名”。如果属性名不明确,可能会给其他开发者造成困扰。

5. 属性名不能以数字开头,必须以字母开头。例如:1name 是不合法的属性名,而 name1 是合法的属性名。

总之,Java Bean 属性的命名规范是非常重要的,它们不仅能够提高代码的可读性,还可以减少代码出错的可能性。因此,在编写 Java Bean 类时,必须遵守 Java Bean 规范,严格按照规定的规则来命名属性。


<font color=#1E90FF size=5>什么是 Java 的内存模型?


Java 的内存模型指的是 JVM (Java 虚拟机) 对内存的管理规范和约束。JVM 中的内存被划分为不同的区域,包括堆区、栈区、方法区、本地方法栈等。Java 内存模型规定了不同内存区域的使用方式、生命周期和访问控制等细节。

Java 的内存模型遵循了以下几个原则:

1. 堆内存用于存储对象实例,栈内存用于存储方法调用过程中的临时变量和参数。

2. 所有的对象引用都存储在栈内存中,指向实际的对象实例所存储的堆内存。

3. 方法区用于存储类信息、常量、静态变量等。

4. 多线程下的内存访问需要保证线程安全,Java 内存模型通过“内存屏障”和“volatile”等机制来保证多线程访问的正确性。

总的来说,Java 的内存模型规定了内存的划分、使用方式和线程安全等方面的规范,使得 Java 开发者能够更加清晰地理解内存的使用方式和多线程访问的正确性。


<font color=#1E90FF size=5>在 Java 中,什么时候用重载,什么时候用重写?


重载(Overloading)和重写(Overriding)都是 Java 中的重要概念。简单来说:

重载:在一个类中定义名称相同但参数不同的方法。

重写:在子类中重新定义父类中已经定义的方法。

重载和重写都是面向对象编程中的多态性的实现方式,但它们的应用场景不同。

适合使用重载的情况:

1. 方法功能相似,但参数列表不同。
2. 方法的返回值类型可以不同。
3. 方法的访问修饰符可以不同。
4. 方法可以在同一个类中定义。

适合使用重写的情况:

1. 子类要对父类方法的实现进行修改或添加特定功能。
2. 子类需要提供自己的实现,以覆盖从父类继承而来的实现。
3. 父类中的方法是抽象方法。
4. 父类中的方法是接口的方法。

总之,在使用重载和重写时,应根据实际情况进行选择,以实现代码的可重用性、灵活性和可维护性。


<font color=#1E90FF size=5>举例说明什么情况下会更倾向于使用抽象类而不是接口?


一般情况下,我们会更倾向于使用抽象类而不是接口的场景:

1. 需要在实现类中复用代码:抽象类可以提供共性实现,避免在实现类中重复编写代码,从而提高代码的复用性和维护性。

2. 需要对代码进行版本控制:如果接口发生变化,那么实现这个接口的所有实现类都需要进行相应的修改,这可能会导致大量的代码更改,而通过使用抽象类,我们可以将代码的共性放在抽象类中,从而减少代码的修改量。

3. 需要传递共性属性:抽象类中可以定义实例变量,而接口中只能定义常量,如果需要在多个实现类中传递相同的属性,那么使用抽象类会更加方便。

4. 需要使用非公共的方法或属性:抽象类可以定义非公共的方法或属性,而接口只能定义公共的方法和常量,如果需要在实现类中使用非公共的方法或属性,那么使用抽象类会更加方便。


<font color=#1E90FF size=5>实例化对象有哪几种方式


```java
Java中实例化对象有以下四种方式:

1. 使用关键字new

```java
MyClass obj = new MyClass();
  1. 使用反射机制
Class<MyClass> clazz = MyClass.class;
MyClass obj = clazz.newInstance();
  1. 使用clone方法
MyClass obj1 = new MyClass();
MyClass obj2 = (MyClass) obj1.clone();
  1. 使用反序列化机制
FileInputStream fileIn = new FileInputStream(

byte类型127+1等于多少

byte类型的范围是-128到127,超出这个范围就会发生越界。在这个问题中,127加1等于-128,因为当最高位(即符号位)为1时,表示为负数,其它位为其二进制表示在反码上加1得到的值。 0x7F(127的十六进制表示)加1会在二进制位上得到0x80,根据二进制补码表示,0x80表示-128。所以byte类型127+1等于-128。

Java 容器都有哪些?

Java 容器主要有以下几种:

  1. List:有序集合,允许重复元素;
  2. Set:无序集合,不允许重复元素;
  3. Queue:队列,通常按照先进先出(FIFO)的顺序处理元素;
  4. Deque:双端队列,可以从队列两端添加或删除元素;
  5. Map:键值对集合,每个键映射到一个值,不允许重复键。

以上容器都有其具体的实现类,例如 ArrayList、LinkedList、HashSet、TreeSet、PriorityQueue、ArrayDeque、HashMap、TreeMap 等。此外,还有一些特殊的容器,例如 Stack(栈)、Vector(向量)等。

Collection 和 Collections 有什么区别?

Collection 是 Java 集合框架中的接口,定义了一组操作和属性,代表了一组元素的集合。

Collections 是 Java 集合框架中一个工具类的名称,提供了一些静态方法,用于对集合对象进行操作,比如排序、查找、复制、反转等方法。其实 Collections 类旨在提供一些静态工具方法,统一了对集合的操作实现,使得许多集合操作变得非常容易。

所以 Collection 是一个接口类型,而 Collections 是一个工具类。两者之间的区别就在于 Collection 主要用于对集合的定义,而 Collections 主要用于对集合操作的实现。

list与Set区别

List和Set都是Java中常用的集合类型,但它们之间有一些重要的区别:

1.是否允许重复元素:List允许元素重复,Set不允许元素重复。
2.是否有序:List是有序的保存元素,即元素在List中的位置是有序的,而Set中元素没有特定的位置。
3.性能:对于查找元素,Set比List更快。对于添加、删除元素,List更快。
4.维护性:当需要对元素进行访问、插入、删除、替换等操作时,List更加方便。而Set更适合于处理不重复元素的情况。

综上所述,根据需求选择使用List或Set。如果需要保存有序元素,并且需要重复元素,则使用List;如果需要不重复元素并且不需要顺序,则使用Set。

HashMap 和 Hashtable 有什么区别?

HashMap和Hashtable都是用于存储key-value键值对的集合。它们的最大区别在于:

1.线程安全性:Hashtable是线程安全的,而HashMap不是。对于封装的数据访问方法,Hashtable使用synchronized方法同步,而HashMap需要外部同步。

2.效率性能:HashMap要比Hashtable快,因为Hashtable是线程安全的,会导致性能降低。

3.空值(null)处理:HashMap可以存储null键和null值,而Hashtable不允许有null键和null值,否则会抛出NullPointerException异常。

综上所述,HashMap适合单线程环境下高效的操作,而Hashtable适合多线程操作。

说一下 HashMap 的实现原理?

HashMap是基于哈希表的一个数据结构,可以实现快速的查找和插入操作。它的实现原理主要包括两个重要的因素:哈希函数和解决哈希冲突的方法。

  1. 哈希函数:它的主要作用是将任意的输入值(键)映射为一个固定大小的输出值(哈希值)。它必须是一个确定性的函数,即对于相同的输入值,哈希函数应该始终返回同样的输出值。Java中HashMap默认的哈希函数是hashCode()方法,该方法返回的是对象的哈希码,但是我们也可以通过重写hashCode()方法来实现自定义的哈希函数。

  2. 解决哈希冲突的方法:当两个不同的键映射到同一个哈希值时,就会出现哈希冲突。HashMap通过开放地址法和链地址法两种方法来解决哈希冲突。

    (1)开放地址法:在发生哈希冲突时,通过一定的规则将键值对存储在数组的其他位置。该方法包括线性探测、二次探测和双重哈希等多种实现方式。

    (2)链地址法:在每个哈希值位置创建一个链表,当出现哈希冲突时,将键值对插入到对应的链表中。

综上所述,HashMap的实现原理就是根据键的哈希值来查找存储位置,并根据哈希冲突解决方法将键值对存储在对应的位置。在向HashMap中添加或删除元素时,需要重新计算哈希值并进行相应的操作,以保证HashMap的正确性和性能。

set有哪些实现类?

Java中Set接口有以下实现类:

  1. HashSet:使用哈希表来实现,不保证元素的顺序。查找和插入的时间复杂度都是O(1),但是哈希表需要较大的存储空间。
  2. LinkedHashSet:在HashSet的基础上,通过双向链表维护元素插入的顺序,保证了元素的顺序。插入和删除的时间复杂度为O(1),但是查找的时间复杂度稍稍比HashSet慢一些。
  3. TreeSet:基于红黑树实现,可以保证元素的有序性。插入,删除和查找的时间复杂度均为O(log n)。
  4. EnumSet:专门用来存储枚举类型的元素,内部采用了位向量的方式实现,具有非常高的效率。
  5. CopyOnWriteArraySet:基于CopyOnWriteArrayList实现,线程安全,支持并发读写操作。插入,删除的时间复杂度为O(n),但是读取的效率非常高,不需要加锁。

说一下 HashSet 的实现原理?

HashSet 是基于 HashMap 实现的。它的实现原理是利用 HashMap 的键来存储数据,值则存储一个固定的对象 (PRESENT)。

在 HashSet 中添加一个元素时,会先将这个元素作为键加入到底层的 HashMap 中,值则为固定的对象。如果 HashMap 中已经存在该键,说明这个元素已经存在于 HashSet 中,因此不需要再次添加;如果不存在,则说明这个元素还未添加到 HashSet 中,需要将其添加到 HashSet 中。

在查询元素时,HashSet 判断元素是否存在的方式同样是利用 HashMap 的键来进行判断。

因此,HashSet 的实现原理主要包括两个部分:底层的 HashMap 存储数据,以及使用固定值作为值,判断数据是否存在。常见的操作包括添加元素、查询元素、删除元素等。

ArrayList 和 LinkedList 的区别是什么?

ArrayList 和 LinkedList 是两种不同的数据结构,最明显的区别在于它们的内部实现方式。

  1. 内部实现:ArrayList 是基于数组实现的,而 LinkedList 是基于链表实现的。

  2. 存储方式:ArrayList 按照索引进行存储和访问元素,LinkedList 按照节点进行存储和访问元素。

  3. 插入和删除操作:在 ArrayList 中,当要插入或删除元素时,需要移动其后面的所有元素,因为数组元素是连续的,而在 LinkedList 中,这种操作是非常高效的,因为只需要更新节点的指针即可。

  4. 访问元素的效率:在 ArrayList 中,访问元素时非常高效,因为可以通过索引来快速访问,而在LinkedList 中,访问元素时相对较慢,因为必须从头开始遍历链表查找要访问的元素。

基于以上的差异,在实际开发中我们可以根据需求选择 ArrayList 或 LinkedList。如果需要高效的访问和修改元素,就应该使用 ArrayList。 如果需要频繁地插入和删除元素,就应该使用 LinkedList。

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

数组和 List 之间可以通过以下方法进行转换:

1. 数组转 List:可以使用Arrays.asList()方法将数组转换为List。例如:

 int[] arr = {
    
    1, 2, 3, 4, 5};
 List<Integer> list = Arrays.asList(arr);

2. List 转数组:可以使用ListtoArray()方法将List转换为数组。例如:

 List<Integer> list = new ArrayList<>();
 list.add(1);
 list.add(2);
 list.add(3);
 int[] arr = list.toArray(new int[list.size()]);

需要注意的是,List 转数组时需要提供一个指定类型的数组作为参数,例如上面代码中的 new int[list.size()],否则会抛出 ClassCastException 异常。对于将 List 转换为数组时类型不匹配的情况,可以使用 Java 8 中新增的 stream() 方法。

List<Integer> list = new ArrayList<>();
 list.add(1);
 list.add(2);
 list.add(3);
 Integer[] arr = list.stream().toArray(Integer[]::new);

3. ListArrayList:如果需要将List转换为ArrayList,则可以直接使用ArrayList类的构造函数。例如:

 List<Integer> list = new ArrayList<>();
 list.add(1);
 list.add(2);
 list.add(3);
 ArrayList<Integer> arrayList = new ArrayList<>(list);

4. ArrayListList:可以直接使用ArrayList类的asList()方法将ArrayList转换为List。例如:

 ArrayList<Integer> arrayList = new ArrayList<>();
 arrayList.add(1);
 arrayList.add(2);
 arrayList.add(3);
 List<Integer> list = arrayList.asList();

总之,Java提供了许多便捷的方法来实现数组和List之间的转换,开发者可以根据实际需求灵活运用。

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

poll() 和 remove() 都可以从队列中移除并返回队首元素,但是它们的行为在队列为空时略有不同。

  • poll() 方法在队列为空时返回 null,而不是抛出异常。
  • remove() 方法在队列为空时抛出 NoSuchElementException 异常。

因此,如果不确定队列是否为空,使用 poll() 方法更安全,因为它不会抛出异常。

哪些集合类是线程安全的

Java提供了许多线程安全的集合类,包括:

  1. ConcurrentHashMap:适用于多线程并发修改的并发哈希表,支持高并发、高效率的访问。

  2. CopyOnWriteArrayList:基于写时复制技术实现的线程安全数组,适用于读操作远远大于写操作的场景。

  3. ConcurrentLinkedQueue:非阻塞队列,适用于高并发环境下的队列场景。

  4. ConcurrentSkipListMap:基于跳表实现的线程安全的有序映射,在并发性能上表现优越。

  5. SynchronizedList、SynchronizedSet、SynchronizedMap:基于同步机制实现的线程安全集合类,但并发性能不如并发集合类。

  6. BlockingQueue:阻塞队列,提供了一种线程安全的数据交换机制,常用于实现生产者-消费者模式。

  7. BlockingDeque:阻塞双端队列,类似于阻塞队列,但可以支持在队尾和队头进行添加和删除操作。

  8. ArrayBlockingQueue:有界阻塞队列,具有固定的容量,适用于有严格容量限制的场景。

  9. LinkedBlockingQueue:可无界阻塞队列,适用于需要缓冲大量数据的场景。

  10. PriorityBlockingQueue:有优先级的阻塞队列,可以按照元素的优先级进行排序。

需要注意的是,并发集合类虽然使用简单,但也需要遵循一定的使用规范才能发挥出其线程安全的优势。

迭代器 Iterator 是什么?

迭代器 Iterator 是一种用于遍历某个集合元素的接口,提供一个统一的方法来访问集合中的每个元素,而不需要暴露其底层实现细节。Iterator 通常会暴露 hasNext()next() 两个方法,用于检查是否还有下一个元素并获取当前元素。Iterator 的存在可以使集合的遍历变得更加简洁、灵活和安全。Python 中的 for 循环实际上是基于迭代器实现的。

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

Iterator 是一种设计模式,主要用于按照集合中的元素逐个访问,并在迭代过程中提供了一种简单、统一的方法来处理集合。

使用 Iterator 可以使得访问和操作集合元素变得更容易,它的特点如下:

  1. Iterator 实现了一种通用的迭代器接口,可以兼容不同集合类型,从而使得编写代码更加简单和灵活。

  2. Iterator 让集合类本身没有必要知道它所包含的元素是什么,只需要实现 Iterator 接口即可,从而使得集合类与元素类实现更加解耦。

  3. Iterator 实现了一种通用的 hasNext() 和 next() 方法用于遍历集合元素,可以通过使用这两个方法提取集合中的下一个元素。

  4. Iterator 可以采用 fail-fast 风格,表示如果在迭代时集合被修改了,那么迭代器就会抛出 ConcurrentModificationException 异常,以保证集合和迭代器的同步性。

总之,Iterator 的主要作用是提供一种通用的、简单的遍历集合元素的方式,用于更方便、灵活的处理集合。

Iterator 和 ListIterator 有什么区别?

Iterator 是一种设计模式,主要用于按照集合中的元素逐个访问,并在迭代过程中提供了一种简单、统一的方法来处理集合。

使用 Iterator 可以使得访问和操作集合元素变得更容易,它的特点如下:

  1. Iterator 实现了一种通用的迭代器接口,可以兼容不同集合类型,从而使得编写代码更加简单和灵活。

  2. Iterator 让集合类本身没有必要知道它所包含的元素是什么,只需要实现 Iterator 接口即可,从而使得集合类与元素类实现更加解耦。

  3. Iterator 实现了一种通用的 hasNext() 和 next() 方法用于遍历集合元素,可以通过使用这两个方法提取集合中的下一个元素。

  4. Iterator 可以采用 fail-fast 风格,表示如果在迭代时集合被修改了,那么迭代器就会抛出 ConcurrentModificationException 异常,以保证集合和迭代器的同步性。

总之,Iterator 的主要作用是提供一种通用的、简单的遍历集合元素的方式,用于更方便、灵活的处理集合。

怎么确保一个集合不能被修改?

Java中,可以通过将集合包装成只读集合来确保其不能被修改。通过使用 `Collections.unmodifiableXXX()` 方法可以创建一个不可修改的集合:

1. 对于 List 类型的集合,可以使用 `Collections.unmodifiableList(List<T> list)` 方法。
2. 对于 Set 类型的集合,可以使用 `Collections.unmodifiableSet(Set<T> set)` 方法。
3. 对于 Map 类型的集合,可以使用 `Collections.unmodifiableMap(Map<K,V> map)` 方法。

队列和栈是什么?有什么区别?

队列和栈都是数据结构中的基本概念。

队列是一种线性数据结构,它按先进先出(FIFO)的原则来排序元素,即最先进队列的元素最先被取出。队列在计算机科学中应用广泛,如在操作系统、计算机网络中常用作缓存、进程调度等。队列有两个基本操作:入队和出队。

栈也是一种线性数据结构,它按照先进后出(LIFO)的原则来排序元素。栈在程序中常用于实现函数调用、表达式求值、括号匹配等。栈有两个基本操作:入栈和出栈。

区别:

队列和栈主要在元素的存储方式和访问方式上有所差异。

队列的插入和删除操作分别在队列的两端进行,插入在队列末尾,称为“入队”,删除在队列头部,称为“出队”,因此队列遵循先进先出的原则。队列常用于实现缓存和任务调度等。

栈的插入和删除操作都在栈的同一端进行,插入称为“压栈”或“入栈”,删除称为“弹栈”或“出栈”,因此栈遵循后进先出的原则。栈常用于实现函数调用、表达式求值和括号匹配等。

因此,队列和栈在数据存储和访问方式方面的差异也导致它们的应用范围存在较大的区别。

Java8开始ConcurrentHashMap,为什么舍弃分段锁?

Java8中的ConcurrentHashMap并没有完全舍弃分段锁,而是通过改进锁升级机制来提高性能。

在旧版本中,ConcurrentHashMap使用了分段锁的机制来保证并发下的线程安全。但是,这种机制会导致分段间的竞争,从而降低性能。

Java8中,ConcurrentHashMap使用了新的加锁机制,即CAS和Synchronized,来代替旧版本中的分段锁。CAS机制用于并发写操作时,而Synchronized则用于并发读操作时的加锁。这两种机制的组合可以减少分段间的竞争,从而提高性能。

此外,Java8中的ConcurrentHashMap还引入了一种新的定位算法,称为TreeBin。当链表长度超过8时,会将链表转化为红黑树,以提高查找效率。

因此,尽管Java8中ConcurrentHashMap舍弃了传统意义上的分段锁,但是采用了新的加锁机制和定位算法来提高性能。

ConcurrentHashMap(JDK1.8)为什么要使用synchronized而不是如ReentranLock这样的可重入锁?

ConcurrentHashMap采用了一种称为分段锁(Segment)的机制来保证并发线程安全,这个机制就是使用了synchronized来进行同步,而不是像可重入锁一样使用ReentrantLock。

这是因为在JDK1.8中,ConcurrentHashMap已经进行了优化,采用了分段锁的方式,每个Segment相当于一个小的HashTable,它们之间相互独立,所以在执行读操作和写操作时,只需要锁住单个Segment即可,不会影响到其他线程对其他Segment的访问。这种方式比使用可重入锁更高效。

另外,synchronized也是JVM内置的线程同步机制,由JVM内部实现来保证同步的正确性,而ReentrantLock则是由程序员显式调用lock()和unlock()方法来控制同步,需要占用较多的系统资源,所以在性能方面可能会有一定的损失。

因此,JDK1.8中的ConcurrentHashMap使用synchronized是出于性能和效率的考虑,并且相对于可重入锁也更为简单和安全。

concurrentHashMap和HashTable有什么区别

  1. 线程安全性的处理方式不同:

ConcurrentHashMap采用了分段锁的机制,将一个大的数据结构拆分成多个小的数据结构,每个小的数据结构配备一个锁,多个线程可以同时访问不同的小数据结构。

而HashTable 采用的是对整个数据结构使用一个锁进行同步,这会导致多个线程竞争同一个锁,导致效率较低。

  1. 初始容量和扩容机制不同:

ConcurrentHashMap的初始容量是16,默认负载因子0.75,当数量超过负载因子*初始容量时,就会扩容,扩容是以2倍的方式进行的。

HashTable初始容量是11,默认负载因子0.75,当数量超过负载因子*初始容量时,就会扩容,扩容是以2倍加1的方式进行的。

  1. 迭代器的设计方式不同:

ConcurrentHashMap的迭代器是弱一致性的,在迭代期间允许其他线程修改集合,这会导致迭代器返回部分修改的结果,但是不会发生抛出异常或死锁等情况。

HashTable的迭代器则是强一致性的,在迭代期间禁止其他线程修改集合,否则会抛出异常。

  1. Null值处理方式不同:

ConcurrentHashMap允许Key或Value为空值,而HashTable则不行,否则会抛出空指针异常。

  1. 性能表现不同

ConcurrentHashMap通过线程安全的设计,大大提高了并发读写能力,适合高并发场景下的使用,而HashTable则因为没有分段锁的机制,所以并发能力不是很强,性能上略逊于ConcurrentHashMap。

综上所述,ConcurrentHashMap比HashTable更适用于高并发的场景下。

HasmMap和HashSet的区别

HashMap和HashSet都是Java集合框架中的类,但是它们有以下几个区别:

  1. 数据结构:

HashMap是基于哈希表的实现,将键和值存储在不同的Entry对象中,利用哈希算法快速查找和定位键值对。

HashSet也是基于哈希表的实现,但是只存储键,每个键都映射到同一个特殊值,即HashSet内部使用的静态变量PRESENT。

  1. 存储对象:

HashMap存储键值对,即key-value对。

HashSet只存储对象,不存储键值对。

  1. 元素顺序:

HashMap中元素的顺序是不固定的,即遍历HashMap时不能保证元素的顺序。

HashSet中元素的顺序也是不固定的。

  1. 数据操作:

HashMap提供了put()、get()、remove()等操作方法。

HashSet提供了add()、remove()、contains()等操作方法。

总的来说,HashMap主要用于存储键值对,HashSet则主要用于存储对象并快速判断对象是否存在。

请谈谈 ReadWriteLock 和 StampedLock

ReadWriteLock 和 StampedLock 都是用于控制共享资源的并发访问的锁机制,但是它们之间存在一些差异:

  1. 概述

ReadWriteLock 可以分为两种类型:读锁和写锁。如果没有线程持有写锁,则多个线程可以同时获得读锁。当一个线程持有了写锁时,则其他线程无法同时获得读锁或写锁,只能等待该线程释放写锁。ReadWriteLock 提供了比 Synchronized 更高的并发性能。

StampedLock 是 Java 8 引入的一种新的锁机制,它也可以支持读写操作。相比于 ReadWriteLock,它提供了更高的并发性能,同时它也支持乐观读取(optmistic read)和悲观读取(pessimistic read)两种模式。

  1. 应用场景

用于读多写少的情况,ReadWriteLock 可以实现多个线程同时读,单个线程写。

StampedLock 适用于读线程远大于写线程的场合。其乐观锁和悲观锁机制,在对数据进行访问时,更为灵活。

  1. 性能比较

在低并发的情况下,两者的性能差别不大;但在高并发情况下,StampedLock 的性能更好,因为它基于乐观锁的机制,避免了对线程的阻塞和唤醒操作。

  1. 操作方法

ReadWriteLock 提供了读锁和写锁两种锁,通过获取/释放这两种锁来控制对共享资源的访问。可通过 ReentrantReadWriteLock 的 readLock() 和 writeLock() 方法来获得相应的锁对象。

StampedLock 有三种访问模式:读锁、写锁、乐观锁。通过调用 stamp() 方法获取戳(stamp),再根据获取到的 stamp 来判断是否需要加锁,或者释放锁。

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

run() 和 start() 是线程的两个方法。

run() 方法定义了线程的主体,包含线程需要执行的代码。这个方法被调用时不会新建一个线程,会在当前线程中执行该方法中的任务。

start() 方法启动一个新的线程,并在新的线程中运行 run() 方法中的代码。因为新建线程的创建与执行需要时间,所以 start() 方法会立即返回并且是非阻塞的。

总之,run() 方法是定义线程内容的方法,而 start() 方法则是启动新线程的方法。使用 start() 方法可以实现多线程、并发执行,而使用 run() 方法则会在当前线程中执行单线程任务。

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

在使用线程时,应该始终调用 start() 方法而不是直接调用 run() 方法。这是因为start()方法执行以下操作:

  1. 将线程标记为可用状态。
  2. 调度线程以便在合适的时间执行run()方法。
  3. 在新线程开始运行前,调用与当前线程相关的所有构造函数。
  4. 启动新线程并执行run()方法。

而直接调用 run() 方法只会在当前线程中执行该方法,不会创建新的线程运行。因此,直接调用run()方法不会实现多线程并行执行的效果,也无法充分利用多核处理器的性能优势。因此,我们应该始终使用start()方法来启动线程并让其自主执行run()方法。

Synchronized 用过吗,其原理是什么?

Synchronized是Java中用来实现线程同步的关键字,其原理是通过互斥锁(Mutex)来保证在同一时间只有一个线程可以执行被synchronized修饰的方法或代码块。

具体来说,当一个线程进入到synchronized代码块中时,会自动获得对象的锁,其他线程将无法访问该代码块,直到该线程执行完毕,释放锁后其他线程才有机会访问这段代码。如果有多个线程同时尝试访问同一个synchronized代码块,那么只有一个线程能够获取到锁,并执行代码块,其它线程将会被挂起等待锁的释放。

可以说,synchronized提供了一种有效的方式用于保证线程安全,避免了多线程情况下对临界资源的竞争和冲突。

JVM 对 Java 的原生锁做了哪些优化?

JVM 对 Java 的原生锁做了以下优化:

  1. 偏向锁:JVM 会在对象创建后,对第一次使用它的线程进行加锁,以提高效率。

  2. 轻量级锁:JVM 会先尝试使用轻量级锁,如果另一个线程正在使用锁,则自旋一次或几次,等待锁的释放。如果等待的时间很长,则转为重量级锁。

  3. 重量级锁:JVM 会将锁升级为重量级锁,这会导致当前线程进入阻塞状态,等待锁的释放。

  4. 自适应锁:JVM 会自动监测锁的使用情况,并根据情况自适应地调整锁的升级策略,以提高程序的运行效率。

  5. 锁消除:JVM 会分析程序的运行情况,判断是否可以消除某些锁操作,以提高程序的运行效率。

这些优化措施可以使锁的使用更加高效、灵活,从而提高程序的性能。

为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?

wait()、notify()和notifyAll()涉及到线程之间的协作问题,需要保证线程之间的同步性,因此必须在同步方法或同步块中调用。具体原因如下:

  1. 线程同步

使用wait(), notify()和notifyAll(),需要保证线程之间的同步。如果不在同步方法或同步块内部调用,就可能导致多个线程同时调用,导致混乱。

  1. 线程状态

使用wait(), notify()和notifyAll(),需要保证线程的状态,否则会导致线程的状态不正确。在同步方法或同步块内部调用,可以保证线程状态的正确性。

  1. 锁对象

使用wait(), notify()和notifyAll(),需要指定锁对象,保证线程之间操作的是同一个锁。如果不在同步方法或同步块内部调用,就无法保证锁对象正确。

综上所述,wait(), notify()和notifyAll()必须在同步方法或同步块中被调用,以确保线程同步,线程状态正确,锁对象正确。

Java 如何实现多线程之间的通讯和协作?

Java中多线程之间可以通过以下方式进行通讯和协作:

  1. 使用共享变量:多线程可以共享变量,通过读写变量的值来通讯和协作。

  2. wait()和notify():一个线程可以调用wait()方法让自己等待,同时释放锁,另一个线程可以在锁被唤醒后调用notify()方法,告诉等待的线程可以继续执行。

  3. join():一个线程可以调用join()方法,等待另一个线程执行完毕后再继续执行。

  4. 管道:多个线程可以通过管道(PipedInputStream 和 PipedOutputStream)互相通讯。

  5. 屏障(CyclicBarrier 和 CountDownLatch):线程可以在某个屏障处等待,直到所有线程都到达该屏障后才能继续执行。

  6. 信号量(Semaphore):可以用来控制一定数量的线程同时执行。

  7. Exchanger:两个线程可以在Exchanger对象上进行数据交换。

总之,Java中提供了多种机制用于多线程之间的通讯和协作,根据具体场景和需求选择合适的方式。

Thread 类中的 yield 方法有什么作用?

在 Java 中,Thread 类的 yield 方法可以让当前正在执行的线程暂停一段时间,让出 CPU 给其他线程执行,等待系统调度器重新调度执行。

它的作用是提高多线程并发执行时的效率,避免某些线程长时间占用 CPU 资源而导致其他线程无法执行的情况。通过调用 yield 方法,可以使操作系统对线程进行重新调度,从而实现多个线程之间的协同执行。

需要注意的是,yield 方法只是一个提示,不能保证线程一定会被调度执行,具体还是需要由操作系统来决定。同时,如果线程没有其他可执行的任务,则调用 yield 方法也不会有任何效果。

为什么说 Synchronized 是非公平锁?

Synchronized 是非公平锁,因为 Synchronized 在锁竞争时未考虑等待锁的先后顺序,而是直接选取头等待线程来获取锁。这就意味着,如果有等待锁的线程,当锁被释放时,Synchronized 将随机选择一个等待线程来获得锁,而不考虑这些等待线程的先后顺序,有可能导致某些线程一直无法获得锁,造成“饥饿”状态。而公平锁则是严格按照等待队列中线程的先后顺序来获取锁,不会出现线程长期等待的情况。所以,Synchronized 是非公平锁。

请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?

volatile 是 Java 提供的一种同步机制,可以保证变量对所有线程的可见性。它的特点包括:

  1. 可见性:对一个 volatile 变量的修改,会立即被其他线程所看到,保证了可见性。

  2. 有序性:volatile 变量的读和写操作与其他变量的读和写操作都按照内存屏障的指令序列执行,保证了有序性。

  3. 不保证原子性:虽然 volatile 能保证可见性和有序性,但不能保证原子性。如果一个变量会被多个线程同时修改,这时就需要使用 synchronized 或者 Lock 等机制来保证原子性。

volatile 变量可以保证可见性的原因是,它在修改变量时会强制刷新到主存中,而其他线程读取该变量时则直接从主存中读取。因此,对于 volatile 变量的修改和读取操作都可以看做一个同步点,保证了可见性。

总之,volatile 变量是一种轻量级的同步机制,主要用于保证变量对所有线程的可见性和有序性。但是由于其不能保证原子性,应谨慎使用。

为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?

Synchronized 是一个悲观锁,因为它在操作前会先获取锁,这意味着它认为其他线程很可能会修改被保护的资源,所以必须通过获取锁来保证线程安全。

乐观锁的实现原理是,在操作前不获取锁,而是假设其他线程不会修改被保护的资源,因此直接进行操作,如果操作成功,则没有问题,如果操作失败,则认为有其他线程在修改该资源,这时要根据具体实现进行重试或者回滚操作。

CAS(Compare And Swap)是一种乐观锁的实现方式,它通过比较当前值和期望值是否相等来判断是否有其他线程对该资源进行了修改,如果相等,则更新该值,否则进行重试或者回滚操作。

CAS 的特性包括:

  1. 原子性:CAS 操作是原子性的,即要么执行成功,要么失败,不存在中间状态。

  2. 无锁优化:由于 CAS 操作不需要获取锁,因此避免了锁的竞争和上下文切换,使得性能较高。

  3. ABA 问题:CAS 操作可能存在 ABA 问题,即在某个线程执行 CAS 操作时,其他线程先修改了该资源,然后再改回了原来的值,导致该线程误判资源未被修改。针对该问题,可以使用版本号等技术进行解决。

乐观锁一定就是好的吗?

不一定。乐观锁虽然具有高并发能力,但在实际应用中仍存在一些弊端,如:

  1. 无法解决脏数据问题。当两个事务同时读取同一条数据时,可能会出现脏数据的情况。

  2. 存在死锁问题。当多个事务同时修改同一组数据时,可能会因为互相等待而导致死锁。

  3. 可能会导致事务回滚。当多个事务同时对同一条数据进行操作时,只有一个事务能够成功提交,其它事务需要回滚。

因此,在实际应用中,需要根据具体的业务场景和需求选择合适的锁机制。

请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。

  1. 同步性质
    synchronized是Java中内建的关键字,ReentrantLock是一个Java类,在java.util.concurrent包中。

synchronized是隐式锁定的,即由Java虚拟机自动获得锁。而ReentrantLock是显式锁定的,需要手动获取和释放锁。这是使用上最直观的区别。

  1. 锁的可中断性
    synchronized锁一旦被获取,就无法中断,只能等待锁释放。ReentrantLock提供了可中断的获取锁和重新获取锁的机制,可中断的获取锁方式可以防止死锁。

  2. 公平性
    在多线程并发的情况下,有时候需要确保每个线程在获得锁的时间上是公平的。synchronized是非公平锁,不考虑线程的先后顺序,只有拿到锁的线程才能运行。而ReentrantLock支持公平锁和非公平锁,默认是非公平锁,若想使用公平锁,可以在创建ReentrantLock对象时将fair参数指定为true。

  3. 锁的实现原理
    synchronized使用的是Java虚拟机实现的一种互斥体(monitor)机制,相当于在代码块或方法上进行加锁和释放锁的操作。ReentrantLock则是通过Java类库实现的,提供了更多的灵活性和扩展性,可以通过ReentrantLock的底层API实现一些Java虚拟机不支持的高级特性。

  4. 锁的性能
    在锁的性能上,synchronized已经很成熟,Java虚拟机为它做了大量的优化工作,使它的性能非常高。ReentrantLock在性能上比synchronized差一些,但如果需要用到它独有的一些特性,性能上比synchronized要好。

  5. 其他
    ReentrantLock提供了Condition接口来实现等待/通知机制,而synchronized中只能通过Object类的wait()和notify()方法实现。

总体来说,synchronized在使用上比较简单,性能也好,在并发量不是很高的情况下使用较为合适,而ReentrantLock则提供了更多的扩展性和灵活性,在高并发、需要加锁和解锁的情况下使用较为合适。

ReentrantLock 是如何实现可重入性的?

ReentrantLock 实现可重入性的方式是通过持有一个对象的锁,并且记录当前线程持有该锁的数量。当一个线程多次获取同一个锁时,会记录该线程持有锁的数量,然后在释放锁时也会相应减少这个数量。

这种方式可以保证一个线程在多次获取同一个锁时不会被自己的持有锁所阻塞。当线程释放完所有持有的锁时,其他线程就可以再次获取该锁。另外,还可以使用 tryLock() 方法来避免死锁的发生,因为它可以尝试获取锁,如果获取失败则会立即返回,避免了线程一直等待锁释放的情况。

总之,ReentrantLock 的可重入性保证了代码的正确性和线程安全性。

什么是锁消除和锁粗化?

锁消除和锁粗化都是针对Java中的锁优化技术。

锁消除是指编译器或即时编译器(JIT)在运行时分析代码,在分析时发现加锁的代码块不会被多线程同时访问,因此编译器会自动将锁消除掉,优化程序的性能。这样就可以避免不必要的加锁操作,提高代码执行效率。

锁粗化是将多个独立的加锁操作,合并成一个大的加锁操作,从而减少加锁和解锁的次数。在实际应用中,很多时候我们需要加锁来保证线程安全,但是频繁地加锁和解锁会带来较大的性能开销。因此,编译器会将多个连续的加锁和解锁操作合并成一个大的加锁操作,从而减少加锁和解锁的次数,提高代码执行效率。

跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?

Synchronized 是 Java 中的关键字,它是一种内置的锁机制,通过 Java 虚拟机来实现,而 ReentrantLock 是一种基于 Java API 实现的可重入锁。

具体而言,Synchronized 的实现是通过 Java 虚拟机来进行的,它通过上锁和解锁的方式来实现同步,在锁的作用域内,其他线程无法访问该锁所保护的代码块,直到该线程解锁后才能访问。因为它是 JVM 内部实现的,所以执行效率很高,但是对于一些复杂的同步问题,可能会导致死锁等问题。

而 ReentrantLock 则是通过 Java API 实现的可重入锁,它通过一个 Lock 接口来实现,支持更复杂的同步操作,并且拥有更强的可定制性。由于它不是 JVM 内部实现,因此灵活性更高,但是相应地,执行效率可能会比 Synchronized 更低。

另外,ReentrantLock 是可重入的,可以被同一个线程多次获得,因此可以避免死锁问题。而 Synchronized 则不支持重入,如果一个线程已经获得了锁,再次请求该锁会导致死锁。

总的来说,ReentrantLock 相比 Synchronized 更灵活、可定制性更强,但相应地也更复杂一些,需要更多的代码实现。

谈谈 AQS 框架是怎么回事儿?

AQS(AbstractQueuedSynchronizer)是 Java 中用于实现同步机制的框架。它是一个抽象类,提供了一些基本的同步操作方法,如 acquire() 和 release(),而不需要直接处理线程的调度和管理。

AQS 的核心是一个双向链表,定义了两种节点:等待队列节点(Node)和同步器状态节点(Sync)。

等待队列节点(Node)是为了记录那些由于某些原因(例如竞争资源、线程调度等)需要等待同步状态的线程,因此用来存储线程信息。等待队列使用双向链表结构,允许在头和尾的节点快速添加或删除。

同步器状态节点(Sync)是用于记录同步器的状态,这些状态包括线程独占、共享和自定义状态。通过同步器状态,可以安全地进行线程调度和资源管理,从而实现真正的同步。

AQS 提供了两种同步方式:独占和共享。独占方式是指只能有一个线程持有同步状态,其他的线程则必须等待该线程释放锁;共享方式是指多个线程可以同时持有锁并访问资源。

通过这种方式,AQS 成为了很多并发框架(例如 ReentrantLock 和 Semaphore)的核心实现。

AQS 对资源的共享有几种方式?

AQS(AbstractQueuedSynchronizer)对资源的共享主要有以下两种方式:

  1. 独占式共享:同一时间内只有一个线程可以获取到该资源的控制权,其他线程必须等待该线程释放资源后才能再次争夺控制权。

  2. 共享式共享:同一时间内多个线程可以同时获取该资源的控制权,适用于读多写少的场景。但是需要注意的是,该方式下的共享不是完全的并行,仍然需要按照先后顺序进行排队。

Java 的线程彼此同步如何实现?

Java 提供了多种机制来让线程彼此同步,包括以下几种方法:

1. synchronized 关键字

可以用 synchronized 关键字来修饰代码块或方法,保证同一时刻只有一个线程能够执行 synchronized 代码块或方法。具体来说,当某个线程进入 synchronized 代码块或方法时,会先获取该对象的锁,其他线程必须等待该线程释放该锁后才能进入 synchronized 代码块或方法。例如:

```java
public synchronized void increment() {
    
    
    count++;
}
  1. wait() 和 notify() 方法

wait() 方法可以让当前线程进入等待状态,直到其他线程调用该对象的 notify() 方法才能继续执行。通常 wait() 方法会放在一个循环中,并检查等待条件是否满足。例如:

synchronized (obj) {
    
    
    while (condition) {
    
    
        obj.wait();
    }
    // 执行代码
}

在另一个线程中,可以调用 notify() 方法通知 wait() 方法处于等待状态的线程继续执行。例如:

synchronized (obj) {
    
    
    condition = false;
    obj.notify();
}
  1. ReentrantLock 和 Condition

ReentrantLock 是一种可重入的互斥锁,可以用来保证同一时刻只有一个线程能够执行临界区代码。Condition 是一个线程等待/通知机制的辅助类,可以用来替代 wait() 和 notify() 方法。例如:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

lock.lock();
try {
    
    
    while (condition) {
    
    
        condition.await();
    }
    // 执行代码
} finally {
    
    
    lock.unlock();
}

lock.lock();
try {
    
    
    condition.signal();
} finally {
    
    
    lock.unlock();
}



<font color=#1E90FF size=5>同步器有哪些?


同步器是一种用于在多线程程序中协调和管理线程之间同步互斥访问共享资源的重要工具。常见的同步器包括:

1.互斥锁(Mutex):一种最基本的同步机制,通过加锁实现多线程之间的互斥访问共享资源。

2.自旋锁(Spinlock):在获取锁的过程中,如果没有获取到就一直循环尝试获取,直到获取到为止。

3.信号量(Semaphore):一种用于控制多个线程访问共享资源的同步机制,具有计数功能。

4.事件(Event):一种通过等待某个标志位的状态变化来通知线程执行任务的同步机制。

5.条件变量(Condition Variable):是一种线程同步机制,用于让多个线程等待特定条件的发生,当条件满足时被唤醒。

6.读写锁(Reader-Writer Lock):一种支持多读单写的同步机制,适合读多写少的场景。

7.屏障(Barrier):一种用于在多个线程之间同步且控制执行顺序的同步机制。

8.原子操作(Atomic Operation):是一种可以保证在多线程环境下不会产生竞争的操作。通常用于实现锁等同步机制。



<font color=#1E90FF size=5>讲一讲Java中的线程池


Java中的线程池是一种多线程处理的技术,它可以在需要时为任务分配一个线程,而不是为每个任务都创建一个线程。这可以减少线程创建和销毁的开销,提高系统的性能和可伸缩性。

Java中的线程池主要由两个部分组成:线程管理器和工作队列。线程管理器用于创建和管理线程,而工作队列用于存储等待执行的任务。当线程池中的线程处于空闲状态时,线程管理器会从工作队列中获取一个任务并分配给空闲线程执行。

Java中提供了三种类型的线程池:FixedThreadPool、CachedThreadPool和ScheduledThreadPool。

FixedThreadPool是指创建一个固定数量的线程池,对于任务数量超出线程池大小的任务,它们将被放置在一个队列中等待执行。

CachedThreadPool是一种可扩展的线程池,它允许在需要时创建新线程。对于在没有空闲线程时提交的任务,它们将被添加到一个队列中并等待执行。

ScheduledThreadPool是一种可以定期执行任务的线程池。它可以按照指定的时间间隔执行任务。

总的来说,线程池是Java中非常重要的一个机制,能够帮助程序员更加高效地管理线程,并在提高程序性能和可伸缩性方面发挥积极作用。



<font color=#1E90FF size=5>创建线程池的几个核心构造参数详细说一下


线程池是一个并发编程中常用的工具,它可以帮助我们管理线程,提高程序性能。在创建线程池时需要配置一些参数,下面是几个核心构造参数的详细解释:

1. corePoolSize:表示线程池中的核心线程数量。当有任务需要执行时,核心线程会立即创建并执行任务。如果线程池中的线程数量大于corePoolSize,而且线程处于空闲状态超过keepAliveTime,那么这些线程会被释放掉,直到线程数等于corePoolSize。

2. maximumPoolSize:表示线程池中最多能够容纳的线程数量。当有任务需要执行但线程数已经达到maximumPoolSize时,任务会被放入等待队列中。如果等待队列已满,那么会根据指定的饱和策略来处理该任务,默认为AbortPolicy。

3. keepAliveTime:表示线程池中的线程空闲时间,超过这个时间就会被回收,直到线程数量等于corePoolSize。该参数只对超过corePoolSize的线程生效。

4. TimeUnit:表示时间单位,可以指定keepAliveTime的时间单位,例如TimeUnit.SECONDS表示秒。

5. workQueue:表示等待队列,用来存储任务的队列,有多种可选择的队列类型,比如ArrayBlockingQueue、LinkedBlockingQueue等。

6. ThreadFactory:表示用来创建新线程的工厂类。该工厂类可以重写newThread方法,以实现自定义线程池中线程的创建。

7. RejectedExecutionHandler:表示饱和策略,当线程池中的线程和等待队列都已满时,会根据指定的饱和策略来处理该任务。默认情况下,饱和策略为AbortPolicy,即抛出异常。

以上就是线程池中几个核心构造参数的详细解释。创建线程池时需要根据具体需求来选择合适的参数设置,以提高程序性能。




<font color=#1E90FF size=5>线程池中的线程创建过程?线程是一开始就随着线程池的启动创建好的吗?


线程池中的线程创建过程通常如下:

1. 线程池初始化时会创建一定数量的线程,通常为核心线程数,这些线程会一直存在直到线程池被关闭。

2. 当有新的任务提交到线程池时,线程池会判断当前运行的线程数是否达到最大线程数,如果没有达到,就会创建新的线程来处理任务,否则任务会被放入等待队列中。

3. 当线程池中的某个线程执行完任务后,它会从等待队列中取出一个任务,如果队列为空就会进入等待状态,如果队列中有任务就会继续执行。

总结来说,线程池中的线程在线程池初始化时会创建一部分,任务提交时如果线程数未达到最大线程数则会再创建新的线程,否则就会进入等待队列中等待执行。

因此,线程不是一开始就随着线程池的启动创建好的,而是根据任务的提交情况动态创建的。



<font color=#1E90FF size=5>volatile 这个关键字的作用是什么


volatile 关键字的作用是告诉编译器,该变量可能在程序的任意时刻被意外地改变,因此编译器不应该对该变量进行优化。一般来说,编译器会对变量进行优化,在程序中使用变量的值时,并不是每次都从内存中读取,而是将其存入寄存器或缓存中,这样可以提高程序的效率。但是,对于某些变量,如硬件寄存器,信号量等,其值可能会在程序未知的时间被改变,此时使用 volatile 修饰符就可以告诉编译器不要进行这样的优化,以保证程序能够正确地使用该变量。



<font color=#1E90FF size=5>基于 volatile 变量的运算一定就是并发安全的吗?


不一定。虽然 volatile 可以保证可见性,但并不保证原子性。对于多个线程同时对同一个 volatile 变量进行修改的情况,可能会出现不正确的结果。因此,如果需要保证原子性操作,仍需使用 java.util.concurrent.atomic 包中提供的原子类,如 AtomicInteger、AtomicLong 等。


<font color=#1E90FF size=5>聊一下ThreadLocal 及其使用场景?


ThreadLocal 是 Java 中的一种线程封闭技术,它为每个线程提供了一个本地变量,实现了不同线程之间变量的隔离,从而避免了线程安全问题。

使用 ThreadLocal 的场景包括但不限于:

1. 数据库连接池,每个线程访问数据库时都需要独立获得一个连接,使用 ThreadLocal 可以为每个线程提供一个连接,避免线程之间出现混乱。
2. 线程池,如果需要在多个任务之间共享数据时,使用 ThreadLocal 可以保证数据在各个线程之间的独立性;另外,也可以为每个任务分配一个独立的线程本地变量,避免并发问题。
3. Web 应用中的用户登录信息,使用 ThreadLocal 可以在用户登录时将用户信息存储到线程本地变量中,方便在用户请求时获取用户信息,从而避免了线程安全问题和冗余的参数传递。

需要注意的是,在使用 ThreadLocal 时需要避免内存泄漏问题,需要在不需要使用 ThreadLocal 变量时及时清理。



<font color=#1E90FF size=5>ThreadLocal 是如何解决并发安全问题的?


ThreadLocal 是一种线程封闭技术,作为一个线程本地的变量,它只能被当前线程访问和修改,其他线程是无法访问这个变量的,因此可以保证线程之间互不干扰。ThreadLocal 使用 ThreadLocalMap 维护线程本地变量,每个线程都有自己的 ThreadLocalMap,于是每个线程都有自己的变量值副本,不会出现多个线程访问同一个变量的情况,从而实现了并发安全。

在多线程并发访问共享变量时,要考虑到线程安全的问题,通常可以使用同步锁来实现,但同步锁会引入额外的开销,导致程序性能下降,而 ThreadLocal 可以避免这个问题。因为 ThreadLocal 变量只有在当前线程中才有意义,所以不需要同步锁来控制多线程访问,避免了锁开销和线程竞争带来的性能开销。

总之,使用 ThreadLocal 可以在多线程并发访问共享变量时保证线程安全,避免线程竞争和锁开销,提高程序的性能和效率。



<font color=#1E90FF size=5>很多人都说要慎用 ThreadLocal,你认为使用 ThreadLocal 需要注意些什么?


1. 内存泄漏问题:ThreadLocal存储的变量是与线程相关的,如果线程不被销毁,变量也会一直存在。如果程序频繁创建线程,很容易发生内存泄漏问题。

2. 线程不安全问题:ThreadLocal虽然是线程本地的,但是当多个线程同时操作同一个ThreadLocal变量时还是会存在线程安全问题,需要注意加锁控制。

3. 静态变量问题:在使用ThreadLocal时应当注意,避免使用静态变量作为ThreadLocal变量的初始值,因为会导致所有线程共享同一个初始值。

4. 垃圾回收问题:在使用ThreadLocal时应当注意及时清理ThreadLocal变量,避免引起垃圾回收问题。

5. 应用场景问题:ThreadLocal适合用于保存与线程密切相关的变量,如用户信息、事务信息等,不应当滥用,否则会导致代码可读性和可维护性下降。



<font color=#1E90FF size=5>聊一下代码重排序


代码重排序是指编译器或处理器会为了提高执行效率而改变顺序或者组合原有的指令序列,其主要原因是为了提高CPU或内存的利用率,缩短程序运行时间或者降低能耗。

具体有以下原因:

1.编译器对代码进行优化,包括重排、删除、修改等操作,以提高程序的性能和可靠性。

2.处理器为了充分利用流水线,会对指令进行重排,使得指令能够顺序执行,避免流水线等待。

3.处理器为了减少内存访问的延迟,在不影响程序语义的情况下,会将一些内存操作指令移到前面,提前访问内存。

总结来说,代码重排序是为了优化程序性能和可靠性,提高处理器和内存的利用率,进而使程序运行得更快、更稳定。



<font color=#1E90FF size=5>讲一下自旋


自旋(Spinning)是Java中的一种同步技术。在多线程环境中,当某个线程请求一个已经被另一线程占用的资源时,它可以等待(阻塞)直到该资源被释放,也可以在一段时间内不停地查询该资源是否被释放,这种不停地查询的过程就是自旋。通过自旋,线程可以减少线程的阻塞等待时间,提高程序的执行效率。自旋的时间可以通过调整spin等待时间来控制。但是如果自旋时间过长,对CPU资源的占用很大,也会影响程序的执行效率。因此,在使用自旋技术时,需要权衡自旋时间和线程阻塞的时间,以实现最佳的程序效率。


<font color=#1E90FF size=5>谈谈多线程中 synchronized 锁升级的原理


在Java中,synchronized是一种线程安全的保证机制,它可以保证在同一时间只有一个线程可以访问一个共享资源。但是在多线程的场景下,synchronized的性能问题成为了一大难题。

为了提高多线程程序的性能,JVM引入了synchronized的锁升级机制。当一个线程首次进入synchronized代码块时,它会增加一个偏向锁,这个偏向锁的目的是让这个线程成为锁的持有者,并且不会被其他线程抢占锁,直到这个线程释放锁。只有当第二个线程尝试获取锁时,锁才会降级为普通的重入锁。

当遇到线程竞争时,偏向锁会升级为轻量级锁。轻量级锁是针对低竞争场景的优化,它不会让线程阻塞,而是通过自旋锁(CAS操作)的方式来获取锁。

如果自旋锁超过一定的时间仍无法获取锁,则会升级为重量级锁。重量级锁是JVM提供的一种保证线程安全的机制,它在处理高竞争场景下可以更好的维护线程安全。

总结来说,synchronized锁升级的原理是针对不同的线程场景选择不同的锁实现,以达到更好的性能和安全性。


<font color=#1E90FF size=5>synchronized 和 ReentrantLock 有什么区别?


synchronized 和 ReentrantLock 都可以保证多线程下的线程安全,但在一些特定场景下,两者存在一些区别:

1. 可重入性:ReentrantLock 是可重入锁,即同一个线程可以多次获取锁,而 synchronized 是不可重入锁。

2. 公平性: ReentrantLock 可以通过构造函数选择是否公平锁,而 synchronized 只能是非公平锁。

3. 等待可中断:ReentrantLock 可以通过 lockInterruptibly() 方法让等待锁的线程能够响应中断,而 synchronized 是不可中断的。

4. 选择性通知:ReentrantLock 可以通过 Condition 对象实现选择性通知,而 synchronized 只能随机唤醒一个或全部线程。

5. 性能:在低并发情况下,synchronized 的性能表现更优,而在高并发下,ReentrantLock 性能更优。

综上所述,ReentrantLock 相对于 synchronized 来说更加灵活,解决了一些 synchronized 不足的地方。但是,ReentrantLock 也需要手动释放锁,使用不当会出现死锁等问题,因此需要谨慎使用。



<font color=#1E90FF size=5>Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?


Lock 接口是 Java Concurrency API 中提供的一种用于同步的工具。它提供了比 synchronized 块更灵活、更强大的同步控制机制。

与 synchronized 不同,Lock 接口可以实现各种灵活的同步需求,包括:

1. 可中断的锁:允许线程在等待锁时被中断,避免死锁。

2. 超时的锁:允许线程在一定时间内等待锁,超时后放弃对锁的请求。

3. 公平锁:允许等待时间最长的线程获得锁,避免饥饿。

4. 多路锁:允许多个线程同时获得和释放锁。

5. 分离锁:允许某个线程获取锁后,将锁转移给另一个线程。

除此之外,Lock 接口还提供了更细粒度的锁获取和释放机制,这种机制能够提升系统的并发性能。

相对于 synchronized,Lock 接口的优势在于它提供了更加灵活的同步控制机制。此外,Lock 接口提供的可中断、超时、公平等特性让程序员有更多的手段控制同步行为,从而避免出现死锁、饥饿等问题。而 synchronized 往往只能做到最基本的同步控制,对于高级需求则比较困难。



<font color=#1E90FF size=5>谈谈jsp 和 servlet的区别?


JSP 和 Servlet 是Java Web 开发中重要的两个技术。

JSP(Java Server Pages)是一种动态网页开发技术,采用类似于 HTML 的语法结合Java代码来生成动态内容。JSP 在服务器端的运行时,会将 JSP 文件编译成 Servlet。

Servlet 是一种在服务器端运行的Java程序。Servlet 运行客户端的请求,并生成动态的响应。Servlet 与 JSP 一样,可以通过 Java 代码生成 HTML 内容。

JSP 和 Servlet 的主要区别在于它们的实现方式。JSP 在底层仍然使用 Servlet 来进行处理,只是在处理过程中会将JSP 转换成 Servlet 以便运行。这也是为什么 JSP 和 Servlet 通常一起使用的原因。同时,Servlet 比 JSP 更加灵活,可以直接与客户端的请求和响应交互,实现更多更灵活的功能。而 JSP 则更适用于界面化的Web应用程序的展示。

总的来说,JSP 和 Servlet 可以互相补充,组合起来可以为开发者提供更加方便灵活的Web开发方式。



<font color=#1E90FF size=5>jsp内置对象及其作用?


JSP内置对象是在JSP页面中自动创建的对象,它们对于JSP页面的开发是非常重要的。常用的JSP内置对象包括以下几种:

1. request:表示来自客户端的HTTP请求报文,包括请求参数、请求头等信息。
2. response:表示服务器对客户端的响应报文,可以使用它来生成HTML内容、设置Cookies、设置HTTP头等。
3. pageContext:表示当前JSP页面的上下文信息,包括Servlet配置信息、请求、响应等对象。
4. session:表示当前客户端与服务器之间的会话。
5. application:表示整个应用程序的上下文信息,包括全局的属性和初始化参数等。
6. out:表示输出流,用于向客户端输出内容。
7. config:表示当前JSP页面的Servlet配置信息。
8. page:表示当前JSP页面本身的一个引用。
9. exception:表示当前页面可能抛出的任何异常对象。

这些内置对象为开发者在JSP页面中获取各种信息和执行各种操作提供了便利,例如获取请求参数、设置Cookies、访问Session、获取ServletContext中的全局属性。通过使用这些内置对象可以方便地实现各种Web应用程序的需求。



<font color=#1E90FF size=5>forward 和 redirect 有什么区别?


forward 和 redirect 都是使用在 Web 应用程序的页面跳转中,但它们的实现方式和效果是不同的。

Forward

Forward 是服务器端的跳转,即在服务器端进行跳转处理,客户端浏览器并不知道这个跳转,也不能直接访问跳转前的页面。Forward 机制是服务器在接收到请求后,对该请求进行处理,并将处理结果直接返回给浏览器。在 Forward 的过程中,浏览器并没有实际请求服务器跳转到的页面,而是由服务器直接把跳转的结果返回给浏览器。

Redirect

Redirect 是客户端的跳转,即在客户端浏览器进行跳转处理,目标页面的 URL 会在浏览器中显示出来。Redirect 实际上是一种重定向机制,采用浏览器重新发起请求的方式来实现页面跳转。

两者的区别

- Forward 是服务器端的跳转,Redirect 是客户端的跳转;
- Forward 是在服务器端直接跳转,客户端浏览器并不知道,Redirect 则是浏览器重新请求跳转后的页面;
- Forward 的效果是跳转后的 URL 不会改变,Redirect 则是会改变 URL;
- Forward 可以携带 request 中的全部信息到跳转后的页面,Redirect 则只能通过 URL 输送一部分信息。

总结

Forward 和 Redirect 都用于页面跳转,但 Forward 是服务器端的转发,Transfer 是客户端的请求。Forward 是通过请求代理,在服务器端直接跳转,而 Redirect 是通过 HTTP 状态码的方式来实现页面跳转。如果需要将请求中的参数都带到跳转后的页面可以使用 Forward,如果需要直接跳转到其他 URL 的页面,那么就使用 Redirect。



<font color=#1E90FF size=5> jsp 的 4 种作用域?


JSP中有四种作用域,分别是:

1. pageScope:页面作用域,是最小的作用域,其内部的变量和对象只能在当前页面中使用,无法在其他页面中被访问。

2. requestScope:请求作用域,变量和对象在一个请求范围内有效,在同一个请求或转发中的JSP页面间可以共享。

3. sessionScope:会话作用域,变量和对象在同一个用户会话范围内有效,在同一个用户会话中的JSP页面间可以共享。

4. applicationScope:应用程序作用域,变量和对象在整个应用程序中有效,可以在所有JSP页面中共享,程序启动后加载一次,会一直保持在内存中。



<font color=#1E90FF size=5>session 和 cookie 区别是什么?


Session 和 Cookie 是用于在网站中跟踪用户的两种不同技术。

Session 是一种服务器端的技术,它将用户信息存储在服务器上。当用户登录网站时,服务器将创建一个唯一的会话 ID,并将其存储在一个名为“session-cookie”的 Cookie 中。然后,服务器使用此唯一 ID 来存储和检索用户信息。Session 通常用于存储用户的登录信息、购物车、表单数据等。

Cookie 是一种客户端的技术,它将用户信息存储在本地计算机上。当用户访问网站时,服务器会将一个或多个 Cookie 发送给用户的浏览器存储。这些 Cookie 包含用户信息,例如登录状态、浏览历史等。当用户再次访问网站时,浏览器将发送存储在 Cookie 中的信息给服务器,以识别用户并提供个性化的体验。

因此,Session 将用户信息存储在服务器上,而 Cookie 将用户信息存储在本地计算机上。Session 通常用于存储保密信息,而 Cookie 通常用于存储偏好设置和用户行为信息。



<font color=#1E90FF size=5>如果在客户端禁止 cookie , session 可用吗?


能,但是需要使用其他方式来存储会话信息,比如 URL 重写或者将会话信息存储在数据库中。不过使用 cookie 来存储会话信息是最常见的方式,因为 cookie 的使用非常方便且跨平台性很好。如果客户端禁用了 cookie,那么就需要采用其他方式来存储和传递会话信息,这样会增加开发难度和复杂度。



<font color=#1E90FF size=5>谈谈你对上下文切换的理解


上下文切换是指在多任务操作系统中,当处理器从一个任务(或进程)切换到另一个任务(或进程)时,必须保存并加载当前任务的上下文信息,包括程序计数器、寄存器、堆栈指针等,以便后续重新调度该任务时能够继续执行。上下文切换是非常耗费系统资源的操作,因为它需要消耗大量的时间和计算资源。在一些特殊情况下,过多的上下文切换会造成系统性能下降,导致系统崩溃等问题。因此,优化上下文切换效率是操作系统设计时需要考虑的重要问题之一。


<font color=#1E90FF size=5>分别介绍一下cookie、session、token


Cookie是一个存储在用户计算机上的小文件,用于在Web应用程序和服务器之间传递信息,它通常用于实现用户认证和持久化登录状态。

Session是指服务器端存储的用户会话信息。当用户访问Web应用程序时,应用程序会在服务器端为该用户创建一个Session对象,并分配一个唯一的Session ID。该Session ID会通过Cookie返回给用户浏览器。用户再次访问应用程序时,浏览器会将Cookie中保存的Session ID发送到服务器端,服务器端则根据此ID找到对应的Session数据,从而维护用户的会话状态。

Token是一种用于认证和授权的安全令牌。其实现方式有多种,常见的有基于JWT(JSON Web Token)的实现方式。当用户登录时,服务器会颁发一个Token,该Token会被保存在浏览器的Cookie或本地存储中,并随后的每一次HTTP请求中发送到服务器,用于证明用户的身份和授权信息。



<font color=#1E90FF size=5>session 的工作原理是什么?


Session是Web开发中常用的一种状态保持机制,它可以在客户端和服务器之间保持一个会话状态,使得我们可以跨越不同的请求来存储和访问信息。Session 的工作原理如下:

1. 客户端访问服务器,并发送请求(比如使用浏览器访问某个网站)。

2. 服务器接收到请求,检查其中是否有Session ID,如果没有则为客户端创建Session ID,并将其返回到客户端。

3. 客户端接收到Session ID,将其储存于cookie中(或者通过URL参数传递)。

4. 下次客户端发送请求时,会将Session ID带上,服务器会根据Session ID找到对应的Session数据。

5. 服务器可以获取或者设置Session数据,并将其发送给客户端或者保存在服务器端。

6. 最后,当用户关闭浏览器或者Session过期时,Session数据会自动从服务器上删除。

总的来说,Session的工作原理就是通过客户端和服务器之间交互的一个唯一标识符(Session ID)来保存用户相关的数据,以实现跨请求的数据共享。



<font color=#1E90FF size=5>响应码 301 和 302 有什么区别?


HTTP响应码301和302都表示重定向(Redirect),具体区别如下:

1. 301表示永久性重定向,而302表示临时性重定向。

2. 当服务器返回301响应码时,表示所请求的资源被永久地移动到了一个新的URL上,而且所有旧的URL都应该自动更新为新的URL。而当服务器返回302响应码时,表示所请求的资源只是暂时性地移动到了一个新的URL上,旧的URL还有可能在未来被使用。

3. 由于301是永久性的,所以浏览器在接收到301响应后会自动更新该URL,在后续访问时会直接请求新的URL。而302是临时性的,浏览器在接收到302响应后会先访问旧的URL,然后再重定向到新的URL。

4. 对搜索引擎的影响不同。由于301是永久性的,所以搜索引擎会把旧的URL的权重转移到新的URL,对SEO有影响,而302则不会影响权重。



<font color=#1E90FF size=5>tcp 和 udp有什么区别?


TCP和UDP是两种不同的传输协议,它们在传输数据时有着显著的区别。

TCP(传输控制协议):

1. 面向连接: 在数据传输前,必须先建立连接,连接包括三次握手和四次挥手,连接建立成功后才能传输数据。

2. 可靠性: 保证传输的可靠性,数据传输错误会进行重传,保证数据准确性。

3. 适用范围:适用于文件传输、邮件传输、网页数据传输等大量传输且要求准确无误的应用场景,如HTTP、FTP等。

UDP(用户数据报协议):

1. 无连接: 不需要建立连接,可以直接发送数据。

2. 不可靠性: 数据传输不保证可靠性,不进行重传,数据丢失无法修复。

3. 快速性: 数据传输不进行确认和重传,传输速度比TCP快。

4. 适用范围: 适用于实时应用,如视频通信、语音通话、网络游戏等,对于数据可靠性要求不高的场景。

总的来说,TCP与UDP在传输数据时的可靠性、连接性、速度等方面存在着明显的差异,需要根据具体场景选择合适的协议。



<font color=#1E90FF size=5>tcp 为什么要三次握手而不是两次两次?


TCP 需要三次握手是为了确保成功建立连接并同步连接双方的状态。具体来说,第一次握手是客户端向服务器发送一个 SYN 报文段,用于请求建立连接;第二次握手是服务器向客户端发送一个 SYN-ACK 报文段,用于确认请求并告知客户端自己也准备好建立连接;第三次握手是客户端向服务器发送一个 ACK 报文段,用于确认服务器的确认。只有当三次握手都成功完成,连接才能正式建立起来。

为什么需要三次握手呢?因为在网络通信中,数据包有可能在传输过程中被丢失、重复或延迟,因此需要握手机制来确保双方同步状态。如果只进行两次握手,那么在客户端发送 SYN 报文段后,有可能该报文段到达服务器并被确认建立连接,但是客户端并没有收到服务器的确认,此时客户端就会认为连接未建立成功,但是服务器实际上已经准备好了。这就可能导致客户端在等待连接建立的同时,服务器会一直占用资源等待客户端发送数据。因此,为了避免这种不确定性,需要进行三次握手来保证连接成功建立。



<font color=#1E90FF size=5>OSI 的七层模型


OSI 七层模型包括以下层级: 

1. 物理层(Physical Layer):物理层是 OSI 模型的最底层,它负责传输比特流,以及数据链路层的电信号。 
2. 数据链路层(Data Link Layer):数据链路层在物理层上增加了透明、可靠的数据传输功能,它负责将比特流粘合成帧,从而组织成连续的比特序列。 
3. 网络层(Network Layer):网络层是整个 OSI 模型的核心,它实现了数据包的传输和路径选择功能。 
4. 传输层(Transport Layer):传输层提供端到端的数据传输,可以将数据分解成较小的数据包,同时确保它们的正确传递和接收。 
5. 会话层(Session Layer):会话层负责建立和维护会话,使得应用程序之间的通信更加高效和安全。 
6. 表示层(Presentation Layer):表示层负责数据格式的转换,以及数据的加密和解密等安全操作。 
7. 应用层(Application Layer):应用层为用户提供了接口,使其可以与计算机系统进行交互,例如发送电子邮件、浏览网页等。



<font color=#1E90FF size=5>get 和 post 的区别



GET请求通常用于从服务器获取某个资源,请求参数会通过URL的查询字符串传递给服务器,可以缓存,但是传参量有限,不太安全;

POST请求通常用于向服务器提交数据,请求参数会放在请求体中传递给服务器,不可以缓存,但是传参量大且相对安全。

总体上说,GET请求适用于请求数据,POST请求适用于提交数据。



<font color=#1E90FF size=5>如何避免XSS 攻击?


XSS(跨站脚本攻击)是一种针对 web 应用程序的安全漏洞,攻击者通过注入恶意脚本代码到 web 页面中,使得当用户访问该页面时,可将其用于盗取用户登录信息、恶意篡改页面内容等,从而造成损失。

避免 XSS 攻击,可以从以下几个方面入手:

1. 过滤输入内容:对用户输入的内容进行过滤,不信任的输入内容进行转义处理。比如,将 < 、>、& 等字符进行转义。

2. 对输出内容进行过滤:对输出到页面上的数据进行过滤,防止用户提交恶意脚本。

3. 采用更加安全的编程方式:使用一些安全编程方法,比如代码审查、输入验证、参数化查询等来避免 XSS 攻击。

4. 配置浏览器:通过配置 Content-Security-Policy 和 HTTP-only Cookie 等,可以增强浏览器的安全性,从而更好地保护页面不被 XSS 攻击。



<font color=#1E90FF size=5>如何避免CSRF 攻击 ?


CSRF 攻击是利用用户已登录的身份,在不知情的情况下,发送伪造的请求到 Web 应用程序。为了避免 CSRF 攻击,可以采取以下措施:

1. 验证请求来源和请求者身份,比如使用 CSRF token。

2. 在提交敏感数据时使用 POST 请求,而不是 GET 请求。

3. 避免使用全局变量存储用户信息,而是使用 session 或 cookie,防止攻击者恶意操作。

4. 避免在 URL 中传递用户身份信息,使用 POST 请求或 header 传递。

5. 检查 Referer 头的值,如果不符合预期,则可能是 CSRF 攻击,应该阻止。

6. 使用验证码技术,可以在表单中加入验证码,防止恶意用户通过程序自动提交。

7. 对于一些敏感操作,增加二次输入密码或者密保,增强安全性。

总之,对于网站的安全性,我们要谨慎对待,提高对安全漏洞的意识,并采取相应的措施避免 CSRF 攻击。



<font color=#1E90FF size=5>如何实现跨域? JSONP 原理?


跨域是指在浏览器中向一个不同域名的资源发送请求。默认情况下,浏览器是不允许这种跨域请求的。因为浏览器实现了同源策略,即不同域名之间的 JavaScript 代码不能相互访问对方的资源。

以下是实现跨域的方法:

1. JSONP

2. CORS(Cross-Origin Resource Sharing)跨域资源分享

3. Proxy 代理

其中,JSONP 是最常见的跨域方法之一。JSONP 全称为 JSON with Padding,它是一种非官方的、但被广泛使用的跨域解决方案。JSONP 的原理是,利用 <script> 标签可以跨域的特性,把 JSON 数据包装在一个函数中,然后作为参数传递到另一个域名的页面中去。这样,另一个页面就可以通过调用这个函数,拿到 JSON 数据。

下面是 JSONP 的一般步骤:

1. 在目标网页中定义一个回调函数

2. 向服务器发送请求,请求中带上这个回调函数的名称

3. 服务器接收到请求后,返回对应的数据,并把数据包装在回调函数中

4. 目标网页接收到回调函数的返回值,执行回调函数

JSONP 的缺点是,只支持 GET 请求,不支持 POST 请求。因为 <script> 标签只能发 GET 请求。此外,JSONP 对于安全性来说也存在一定的隐患,因为在请求的过程中,服务器端的代码可以动态地注入任意内容,从而造成潜在的安全风险。


<font color=#1E90FF size=5>说一下 tcp 粘包


TCP粘包是指发送方在发送数据时,由于发送数据速度过快或者数据包过小,导致多个小数据包被一起发送到接收方,从而“粘”在一起。接收方在接收数据时,由于缓冲区大小限制或者内部处理速度慢,可能无法及时处理块状数据,只能一次性接收多个数据包,从而无法区分数据包之间的边界,这就是TCP粘包产生的原因。

TCP粘包产生的原因主要有以下几个:

1. 发送端原因:发送方在短时间内发送多个小的数据包,由于TCP协议的流量控制,可能会产生发送拥塞的情况,使得多个数据包被一次性发送到接收方。

2. 接收端原因:接收方在接收数据时,由于缓冲区过小或者内部处理速度不够,无法及时处理块状数据,只能一次性接收多个数据包,从而无法区分数据包之间的边界。

3. 网络原因:在网络传输过程中,TCP协议使用的数据包可能会被拆分成多个分段进行传输,而这些分段可能会被重新组合成更大的数据包,从而出现TCP粘包的情况。

为了避免TCP粘包的产生,通常采取的措施包括:

1. 设置合适的发送间隔和数据包大小,以避免发送拥塞的情况。

2. 在数据包中添加特殊标志或边界信息,以便接收方区分不同的数据包。

3. 使用消息队列或者缓冲区来存储接收到的数据,异步读取并进行处理,避免数据包在接收时被合并。



 <font color=#1E90FF size=5>JDK 中几个常用的设计模式列举一下?


1. 工厂模式 (Factory Pattern) - 用于创建对象,通过这种方式,我们无需关心对象的创建过程,只需要关心所需的对象即可。
2. 单例模式 (Singleton Pattern) - 限制一个类只能创建一个对象,保证在整个应用程序中该类只存在一个实例。
3. 装饰器模式 (Decorator Pattern) - 允许向一个现有对象添加新的功能,同时又不改变其结构。
4. 观察者模式 (Observer Pattern) - 在对象间定义一对多的依赖关系,当一个对象发生改变时,所有依赖于它的对象都会得到通知并更新。
5. 适配器模式 (Adapter Pattern) - 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。
6. 策略模式 (Strategy Pattern) - 定义一个家族算法,将每个算法封装起来,使它们之间可以相互替换。 
7. 模板方法模式 (Template Method Pattern) - 定义一个操作中的算法的骨架, 而将一些步骤延迟到子类中。
8. 建造者模式 (Builder Pattern) - 将一个复杂对象的构建过程与它的表示分离,使得相同的构建过程可以创建不同的表示。
9. 代理模式 (Proxy Pattern) - 为其他对象提供一种代理以控制对这个对象的访问,可以起到保护对象的作用,同时也可以用于优化对象的创建和销毁过程。
10. 迭代器模式 (Iterator Pattern) - 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该聚合对象的内部表示。



<font color=#1E90FF size=5>什么是设计模式?


设计模式是指在特定情境下,解决某个问题的一套通用的解决方案。它是经过实践验证的,具有可重复使用性的设计思想。


<font color=#1E90FF size=5>介绍一下单例设计模式,并写出线程安全的单例模式


```java
单例设计模式是一种创建模式,它保证了一个类只有一个实例,并提供了全局访问点。在某些场景下,使用单例模式可以很好地控制系统资源的使用,提高系统性能。

下面是一种线程安全的单例模式实现:

```java
public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在这个实现中,getInstance() 方法使用了 synchronized 关键字,确保了线程安全。当多个线程同时访问 getInstance() 方法时,只有一个线程能够获得锁,进入方法体进行实例化操作。其他线程需要等待锁的释放。注意,synchronized 的使用会带来性能开销,可以考虑使用双重检查锁定等方式进行优化。




<font color=#1E90FF size=5>观察者设计模式是什么?


```java
观察者设计模式是一种软件设计模式,其定义对象之间的一对多依赖关系,使得当一个对象状态发生改变时,所有依赖它的对象都能自动得到通知并且被更新。

在 Java 中,观察者模式包括两个主要角色:观察者和被观察者。具体实现中,被观察者维护着一个观察者列表,每当被观察者状态改变时,它会遍历观察者列表并调用每个观察者的通知方法,让它们进行相应处理。

以下是一个简单的用代码实现的观察者模式示例:

```java
// 观察者接口
public interface Observer {
    void update(int state);
}

// 被观察者接口
public interface Observable {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(int state);
}

// 被观察者实现类
public class ConcreteObservable implements Observable {
    private List<Observer> observers = new ArrayList<>();
    private int state;

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(int state) {
        for (Observer observer : observers) {
            observer.update(state);
        }
    }

    public void setState(int state) {
        this.state = state;
        notifyObservers(this.state);
    }
}

// 观察者实现类
public class ConcreteObserver implements Observer {
    @Override
    public void update(int state) {
        System.out.println(

工厂模式最主要的好处及使用场景?

工厂模式最主要的好处是将对象的创建与使用分离,可以更加灵活地创建对象并降低类之间的耦合性。在需要创建多个相似对象的情况下,使用工厂模式可以简化代码实现并提高代码的可维护性和可扩展性。

工厂模式通常应用于以下场景:

  1. 当需要大量创建某个类的对象时,使用工厂模式可以降低代码复杂度和重复代码的数量;
  2. 当对象的创建过程较为复杂,需要根据一些条件来判断创建哪种对象时,使用工厂模式可以将复杂的创建过程封装起来,提高代码的可读性和可维护性;
  3. 当需要向客户端隐藏具体对象的实现细节时,使用工厂模式可以通过工厂方法来获取对象,保护对象的私有信息。

不同自动装配模式的区别?

自动装配(autowiring)是Spring的一个核心特性,它处理了两个组件之间的依赖关系。在Spring中,有三种自动装配模式:

  1. byName自动装配

byName自动装配是根据bean的名称来自动连接bean之间的依赖关系。当Spring容器需要注入一个bean的时候,它会根据setter方法中的属性名和容器中的bean名称进行匹配,如果匹配成功,就将bean注入进来。这种方式需要在xml配置文件中定义bean的名称,在setter方法中指定属性名称。

  1. byType自动装配

byType自动装配是根据bean的类型来自动连接bean之间的依赖关系。当Spring容器需要注入一个bean的时候,它会根据setter方法中的参数类型和容器中的bean类型进行匹配,如果匹配成功,就将bean注入进来。这种方式需要在xml配置文件中定义bean的类型。

  1. constructor自动装配

constructor自动装配是根据构造函数的参数类型来自动连接bean之间的依赖关系。当Spring容器需要创建一个bean的时候,它会根据构造函数的参数类型和容器中的bean类型进行匹配,如果匹配成功,就将bean创建出来。这种方式需要在xml配置文件中定义构造函数的参数类型。

以上三种自动装配模式都可手动关闭,避开自动装配机制进行手动联线。

举一个用 Java 实现的装饰模式(decorator design pattern)?它是作用于对象层次还是类层次?

一个用 Java 实现的装饰模式可以如下:

1. 定义一个抽象类 `Shape`,表示图形对象,有一个抽象方法 `draw()`。

```java
abstract class Shape {
    
    
    abstract void draw();
}
  1. 定义一个具体的图形类 Rectangle,实现 Shape 接口并实现 draw() 方法。
class Rectangle extends Shape {
    
    
    void draw() {
    
    
        System.out.println(

介绍一下Spring 框架及其主要模块?

Spring框架是一个开源的Java应用框架,它是企业级应用程序开发的首选框架之一。它提供了一种简单的编程模型,使得开发者可以快速构建出以面向切面编程、依赖注入和控制反转为特点的应用程序。

主要模块包括:

1.核心容器(Spring Core):提供了整个框架的核心组件,包括BeanFactory和ApplicationContext。

2.依赖注入(Spring DI):实现了IoC和依赖注入,实现了在应用程序中的各个组件之间通过注解或XML配置上下文中的Bean对象来实现松散耦合的编程。

3.面向切面编程(Spring AOP):提供了一种用于开发程序中横切关注点的框架,以实现代码的高复用性和低耦合性。

4.数据访问(Data Access):提供了支持JDBC、ORM、ORM框架的集成和DAO支持。

5.Web框架(Spring MVC):提供了一种基于MVC设计模式的web应用开发框架。

6.测试(Spring Test):提供了测试Spring应用程序的支持。

7.其他模块:如Spring Security、Spring Web Services、Spring Batch等。

使用 Spring 框架优势?

  1. IoC(控制反转):Spring 提供了一个容器,可以在容器中解决依赖关系,从而将对象的创建和依赖解决转移到框架中,使得开发者可以更加专注于业务逻辑而不是对象创建和管理。

  2. AOP(面向切面编程):Spring 支持 AOP,可以将逻辑横跨多个方法和对象的代码,封装到一个切面中。这样,应用程序可以更加清晰,易于维护。

  3. MVC(模型—视图—控制器)模式:Spring 提供了一个 Web 框架,可以使用 MVC 模式开发 Web 应用程序。

  4. JdbcTemplate:Spring JDBC 支持提供了 JdbcTemplate,可以使数据库操作变得更加容易、优雅。通过 JdbcTemplate 提供的方法,可以从 ResultSet 中提取数据,并将其转换成适合于应用程序的格式。

  5. 支持声明式事务:Spring 支持声明式事务处理,以及多个事务管理器。这简化了开发人员处理事务的过程,同时还提高了应用程序的性能和可靠性。

  6. 集成其他框架和类库:Spring 可以很好地集成其他框架和类库,例如 Hibernate、Struts 等等。

  7. 简化测试:Spring 提供了一个测试框架,使开发人员可以更容易地测试其应用程序的不同部分。

  8. 提高可维护性:通过将对象创建和依赖解决转移到框架中,Spring 能够使应用程序变得更加容易维护和管理。

  9. 可扩展性:Spring 的架构基于松散耦合的原则,这使得 Spring 应用程序可以更容易扩展。

  10. 统一接口:Spring 提供了统一的接口,使得开发人员可以进行切换,例如更改请求路径、数据库访问、ORM 等等。

Spring IOC、AOP举例说明

Spring IOC (Inversion of Control)是一种设计模式,它将对象的创建和依赖注入的过程转移到容器中来动态管理,从而实现松耦合和可维护的程序。举个例子,在Spring中,我们可以定义一个Bean(对象)类,并将其注入到Spring容器中,容器会负责实例化该Bean并管理其完整的生命周期,当我们需要使用该Bean时,只需要从容器中获取该Bean的实例即可,而不必关心该Bean是如何创建和注入依赖的。

Spring AOP (Aspect Oriented Programming)是一种面向切面编程的技术,它通过动态代理和拦截器机制,在程序运行期间动态地为类和方法添加额外的行为,从而实现横向业务逻辑的复用和分离。举个例子,在Spring中,我们可以定义一个切面(Aspect)类,该类可以定义一些通知(Advice)方法,并通过切点(Pointcut)表达式指定需要对哪些类或方法进行增强,当程序运行到指定的类或方法时,Spring容器会自动地将切面类的通知插入到该方法中从而实现业务逻辑的增强。比如,我们可以通过AOP来实现日志、事务、安全等横向业务逻辑的复用。

说一下控制反转IOC和AOP

控制反转(Inversion of Control,简称IoC)是一种设计模式,它将一个应用程序的控制权从代码本身中移动到外部容器中,让框架或容器负责对象的创建、组装和管理。这样,应用程序只需要提供必要的配置信息,而不需要直接创建对象和管理它们的生命周期。

依赖注入(Dependency Injection,简称DI)是IoC的一种实现方式,它通过容器来管理对象之间的依赖关系并将这些依赖关系注入到对象中,从而达到解耦的目的。具体来说,DI让对象在创建时将其依赖的对象传递进来,或者在需要使用依赖时通过依赖注入的方式获取依赖对象,而不需要自己创建或管理依赖对象,从而实现了对象之间的解耦。

BeanFactory 和 ApplicationContext 有什么区别?

BeanFactory 和 ApplicationContext 是两个 Spring 容器的实现,它们在某些方面有所不同。

  1. 加载时间:BeanFactory 被继承使用而存储每一个对象的定义(比如配置文件中的 bean)。这意味着,只有在第一次使用 bean 时它的实例才会被创建。而 ApplicationContext 则在容器被初始化后,自动加载所有的 singleton bean。

  2. 额外功能:ApplicationContext 扩展了 BeanFactory,提供了更多额外的功能,如国际化处理,事件发布允许不同 bean 之间进行交互等。

  3. 依赖加载方式:BeanFactory 是延迟加载,通过调用 bean 的 getBean() 方法来实例化一个 bean。ApplicationContext 是在容器启动时,一次性加载所有的 bean,从而提高容器的性能。

  4. 配置方式不同:BeanFactory 容器需要手动配置它所需要的 bean。ApplicationContext 支持如 XML、Java Annotations、Java 配置文件等多种配置方式,描述 bean 的定义信息。

综上所述,BeanFactory 提供基本的核心功能,而 ApplicationContext 则提供更多额外的功能。并且,在性能上 ApplicationContext 更佳,因为程序只需要一次性加载所有的 bean,同时使用更多的配置方式。

什么是 JavaConfig?

JavaConfig 是 Spring 框架的一种配置方式,也被称为基于 Java 的配置,它允许以纯 Java 代码的形式进行 Spring Bean 的定义和配置,而不是使用 XML 文件。通过 JavaConfig,可以更加清晰和简洁地定义和组织复杂的应用程序结构,避免了 XML 配置文件的复杂和冗长。JavaConfig 从 Spring 3.0 版本开始引入,是一个可选的、类型安全的、面向对象的 Spring 配置方式。

什么是 ORM 框架?

ORM(Object Relational Mapping)框架是一种程序设计技术,用于将数据库中的关系数据映射到对象模型中,使得开发人员可以通过面向对象的方式去操作数据库,而不必关心底层的 SQL 语句及数据库的具体操作。ORM 框架减少了重复的 CRUD(Create、Read、Update、Delete)代码,提高了开发效率和代码的可读性,常见的 ORM 框架包括 Hibernate、MyBatis、SQLAlchemy 等。

Spring 有几种配置方式?

Spring 有三种主要的配置方式:

  1. XML 配置方式:通过编写 XML 文件来配置 Spring 应用上下文,这是最传统也是最常用的方式。

  2. Java 配置方式:通过编写 Java 代码来配置 Spring 应用上下文,可以使用 Spring 提供的注解或者编写 Java 配置类。

  3. 注解配置方式:通过在 Spring Bean 类上使用注解来配置 Spring 应用上下文,虽然无需编写 XML 或 Java 配置文件,但代码与 Spring 框架耦合度较高,不太建议在大型项目中使用。

请解释 Spring Bean 的生命周期?

Spring Bean 的生命周期包括以下几个阶段:

  1. 实例化:当容器接收到创建 Bean 的请求时,会根据配置信息或类定义来实例化一个对象。

  2. 属性赋值:为 Bean 的属性赋值,包括依赖注入。

  3. BeanPostProcessor 处理器的处理:在初始化前后对 Bean 进行额外的处理。

  4. 初始化:调用 Bean 的初始化方法。

  5. 容器使用:此时 Bean 已经可以被容器使用了。

  6. 销毁:当容器关闭或者 Bean 被移除时,容器会调用 Bean 的销毁方法。

在上述过程中,BeanPostProcessor 接口扮演着很重要的角色,它提供了初始化前后的处理方法,可以用来实现一些自定义的处理逻辑,比如数据校验、权限校验等。同时,Spring 还提供了一些常用的 BeanPostProcessor 实现类,比如:AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor 等。

Spring Bean 的作用域之间有什么区别?说一下Spring容器中的bean5个范围:

  1. Singleton:默认范围,只会创建一个 Bean 实例,所有请求都会返回同一个实例。
  2. Prototype:每次请求都会创建新的 Bean 实例。
  3. Request:每一次 HTTP 请求都会创建一个新的 Bean 实例,仅适用于 WebApplicationContext 环境。
  4. Session:每一次 HTTP 会话都会创建一个 Bean 实例,仅适用于 WebApplicationContext 环境。
  5. GlobalSession:与 Session 类似,但是用于全局会话,仅适用于 PortletApplicationContext 环境。

如何在 Spring Boot 中禁用 Actuator 端点安全性?

要禁用 Spring Boot Actuator 端点安全性,可以执行以下步骤:

  1. application.properties 文件中添加以下属性:
management.security.enabled=false
  1. 如果您使用的是 YAML 文件,则可以在 application.yml 中设置以下属性:
management:
  security:
    enabled: false
  1. 保存并重新启动应用程序。Actuator 端点现在应该不再需要安全认证。

请注意,禁用端点安全性可能会对应用程序的安全性带来潜在风险。建议仔细考虑禁用 Actuator 端点安全性的风险并根据应用程序的安全需要进行配置。

什么是 Spring inner beans?

Spring inner beans 是指在 Spring 容器中创建的嵌套 Bean,它们不需要在 XML 或 Java 配置文件中进行显式声明。Inner beans 只能在外部 Bean 中使用,它们不像「bean」标签那样,具有 ID 属性,因此不能在容器中进行直接引用,只能通过引用外部 Bean 的方式对其进行引用和使用。通常情况下,Spring 内部 Bean 的使用是为了提高程序的可维护性和可读性,尽量把与特定功能相关的 Bean 并在一起,使其更清晰和整洁。

Spring 中的单例 Beans 是线程安全的么?

是线程安全的。Spring 框架中的单例 Beans 默认情况下是单线程环境下的。Spring 容器会保证在整个应用程序中只有一个 Bean 实例存在,并可以在多个线程中共享该实例。因此,Spring 容器内的单例 Beans 不需要开发者使用额外的同步机制来确保线程安全性。但是,开发者需要注意编写线程安全的代码来访问 Beans 实例中的属性和方法。

请解释 Spring Bean 的自动装配?

Spring Bean 的自动装配是指 Spring 容器根据特定的规则自动将一个 Bean 注入到另一个 Bean 中。自动装配的目的是减少开发人员在配置过程中的多余工作,提高开发效率。

Spring 容器支持多种自动装配方式,包括 byName、byType、constructor 等。具体规则如下:

  1. byName:容器会通过 Bean 的 id 与属性名进行匹配,如果匹配成功,则该 Bean 可以自动注入到属性中。

  2. byType:容器会通过属性的类型与容器中的 Bean 进行匹配,如果匹配成功,则该 Bean 可以自动注入到属性中。

  3. constructor:容器会尝试寻找与 Bean 构造函数参数类型匹配的 Bean,如果匹配成功,则该 Bean 可以自动注入到构造函数中。

而且,Spring 还提供了 @Autowired 注解,可以标注在属性、构造函数、方法上,让容器自动注入相应的 Bean。如果在容器中找不到匹配的 Bean,则会报错。同时,也可以通过 @Qualifier 注解指定具体的 Bean 进行装配。

如何开启基于注解的自动装配?

  1. 在 Spring Boot 应用中,只需在主配置类上添加 @EnableAutoConfiguration 注解即可开启基于注解的自动装配。

  2. 如果需要指定一些自动配置类进行加载,可以在 @EnableAutoConfiguration 上指定 excludeexcludeName 参数,来排除一些自动配置类的加载。

  3. 可以使用 @ConditionalOn* 注解进行条件装配,例如 @ConditionalOnClass@ConditionalOnExpression 等,只有满足条件才会进行自动装配。

  4. 可以通过修改配置项来开启或禁用指定的自动配置项,例如 spring.autoconfigure.exclude 配置项可以指定禁用的自动配置类,spring.autoconfigure.include 配置项可以指定开启的自动配置类。

  5. 如果需要实现自定义的自动配置,可以使用 @Configuration@EnableConfigurationProperties 注解来定义自己的配置类,然后在主配置类中使用 @Import 引入自定义配置类即可实现自动装配。

讲一下 Spring Batch?

Spring Batch是一个批处理框架,用于处理大量数据、ETL(Extract, Transform, Load)等批处理应用。它提供了全面的批处理功能,包括事务管理、分片处理、分组处理、并行处理、错误处理、状态管理等,能够满足各种批量处理应用的需求。Spring Batch基于Spring框架,可以轻松地与其他Spring组件,如Spring MVC、Spring Boot等集成。

spring mvc 和 struts 的区别是什么?

  1. 构造方式不同:Spring MVC 由于是 Spring 框架的一部分,它的构造方式是基于一个中央控制器(DispatcherServlet),它负责接收所有 HTTP 请求并将请求分派给相应的处理程序。而 Struts 采用了一种基于配置文件的方法,使用 struts.xml 配置文件来将请求映射到处理程序。

  2. 面向服务不同:Spring MVC 是一个更为通用的框架,它鼓励面向服务的体系结构和 RESTful Web 服务。而 Struts 在其早期版本中主要是面向页面的,它更加关注的是页面和表单的处理。

  3. 拓展性不同:Spring MVC 具有更强的扩展性,它允许用户定制并添加自定义功能,而 Struts 则相对较少采用插件或其他形式的扩展机制。

  4. 数据绑定不同:Spring MVC 提供了多种数据绑定选项,支持多种数据类型,包括日期和时间,而 Struts 则相对较少提供这些数据绑定选项。

  5. 异常处理不同:Spring MVC 实现了一种基于注解的异常处理机制,可以在代码层面上通过使用 @ExceptionHandler 和 @ControllerAdvice 注解来捕获异常,而 Struts 则将异常处理集成到其所有的操作中。

  6. 测试方法不同:Spring MVC 提供了一种单元测试框架,可以方便地对控制器进行测试,而 Struts 则需要编写基于集成测试框架的测试代码来完成相同的测试。

解释@Required 注解:

@Required 注解是 Spring 框架提供的一个注解,它用于标注在类的属性上,表示该属性必须在 XML 配置文件中进行声明和初始化。如果在 XML 配置文件中没有声明或初始化该属性,程序启动时就会报错。

Spring常用注解

  1. @Autowired:自动装配依赖注入
  2. @Component:标识该类为组件类,告诉Spring 对该类进行创建、管理和依赖注入
  3. @Service:标识该类为Service层组件类
  4. @Repository:标识该类为DAO层组件类
  5. @Controller:标识该类为控制器组件类
  6. @RequestMapping:处理请求的uri映射
  7. @ResponseBody:声明Controller的接口返回值为json格式数据
  8. @PathVariable:获取url中的参数值
  9. @RequestParam:获取请求参数值
  10. @CrossOrigin:支持跨域请求
  11. @Transactional:声明方法需要事务管理
  12. @Cacheable:声明方法需要使用缓存
  13. @Async:声明方法需要异步执行
  14. @Value:注入属性值
  15. @PostConstruct:在构造函数之后初始化属性赋值完成后执行的方法
  16. @PreDestroy:销毁类之前执行的方法
  17. @Configuration:标识该类为Spring配置类
  18. @Bean:标识该方法返回一个Bean,Spring会将其加入容器供应用程序使用
  19. @Import:将其他的@Configuration类导入到当前配置类中
  20. @Scope:声明Bean的作用域

如何实现权限验,需要几张表

权限验证可以通过使用用户表、角色表、权限表、和用户-角色-权限关联表来实现。

用户表包含用户的信息,例如用户名和密码。角色表包含角色的信息,例如角色名称和描述。权限表包含权限的信息,例如权限名称和描述。

用户-角色-权限关联表将这三个表联系起来,以便确定哪些用户具有哪些角色和权限。此表包含三个外键:用户ID、角色ID和权限ID。

当用户登录时,系统会验证其用户名和密码是否正确,并从用户-角色-权限关联表中获取该用户具有哪些角色和权限。然后,在进行某些操作(例如访问某些页面或执行某些功能)时,系统将检查该用户是否具有所需的角色和权限。如果该用户不具备必要的角色和权限,则将拒绝其请求。

因此,实现权限验证需要用户表、角色表、权限表和用户-角色-权限关联表这四张表。

谈谈controller,接口调用的路径问题

Controller是MVC模式中的C,是负责接受用户请求,处理请求并返回响应的组件。简单来说,Controller就是一个Java的类(或其他语言的类),用于处理请求并返回响应的逻辑代码。

在一个Web应用程序中,Controller可以被认为是应用程序后端的中枢部件。 它负责解析入站请求,以确定要执行的操作,并生成出站响应以返回给客户端。Controller通过路由机制进行注册和调用,确保所有请求都由正确的Controller处理。

对于接口调用的路径问题,Controller的路径是由路由规则确定的。Web框架通常提供路由规则设置接口,比如Spring框架中的@RequestMapping注解。开发人员可以在Controller方法上添加该注解,并指定该方法的请求路径。

Controller是MVC模式中的C,是负责接受用户请求,处理请求并返回响应的组件。简单来说,Controller就是一个Java的类(或其他语言的类),用于处理请求并返回响应的逻辑代码。

在一个Web应用程序中,Controller可以被认为是应用程序后端的中枢部件。 它负责解析入站请求,以确定要执行的操作,并生成出站响应以返回给客户端。Controller通过路由机制进行注册和调用,确保所有请求都由正确的Controller处理。

对于接口调用的路径问题,Controller的路径是由路由规则确定的。Web框架通常提供路由规则设置接口,比如Spring框架中的@RequestMapping注解。开发人员可以在Controller方法上添加该注解,并指定该方法的请求路径。

@RequestMapping("/springmvc")
@Controller
public class SpringMVCTest {
    
    
    @RequestMapping("/testRequestMapping")
    public String testRequestMapping(){
    
    
        System.out.println("testRequestMapping");
        return SUCCESS;
    }
}

Spring中都应用了哪些设计模式

  1. 单例模式:Spring的Bean默认都是单例的,保证一个类实例只会被创建一次。

  2. 工厂模式:Spring中的BeanFactory就是一个工厂模式的实现,它通过getBean()方法返回Bean实例。

  3. 观察者模式:Spring的事件驱动机制就是采用观察者模式实现的,通过监听事件可以触发相应的操作。

  4. 模板方法模式:Spring的JdbcTemplate就是一个典型的模板方法模式,它封装了JDBC的操作过程,并定义了一个模板方法让子类实现。

  5. 适配器模式:Spring的适配器模式主要体现在MVC框架中,适配器将请求和处理器进行适配,使得处理器能够处理请求。

  6. 策略模式:Spring的策略模式主要体现在AOP技术中,通过不同的切面来实现不同的功能。

  7. 代理模式:Spring的AOP也采用了代理模式的实现,通过代理对象来实现目标对象的增强。

  8. 外观模式:Spring的外观模式主要体现在Spring框架的配置文件中,将复杂的配置过程封装在配置文件中,提供简单的接口给用户使用。

如何在 Spring 中注入一个 Java Collection?

Spring注入有四种方式,

set注入;
构造器注入;
基于注解的注入;
xml配置文件注入;
想要注入java collection,就是注入集合类:

list
set
map
props:该标签支持注入键和值都是字符串类型的键值对。
list和set都使用value标签;map使用entry标签;props使用prop标签;

讲一下mybatis 中 #{}和 ${}的区别是什么?

在MyBatis中,#{}和${}都是用于动态SQL语句中的占位符。它们的主要区别在于:

  1. #{ }是预编译语句,将传入的值作为参数进行占位,避免SQL注入的风险,可以防止一些潜在的SQL注入攻击。因为MyBatis会将这样的参数自动进行处理,使得传入的数据符合SQL语法规则。

  2. ${ }是字符串替换,在SQL语句中直接替换变量值,可以用于传递一些常量和字符串类型的变量。因为它是字符串替换,所以需要注意在构造SQL语句时潜在的SQL注入风险。

综上所述,#{}更加安全可靠,适合于传递变量值,而${}适合于传递常量和字符串类型的变量。建议在MyBatis中尽可能使用#{ }语法。

mybatis 支持延迟加载吗?其原理是什么?

是的,MyBatis支持延迟加载。

延迟加载的原理是,当查询结果中包含多个对象时,MyBatis并不会立即将所有对象的相关信息全部加载到内存中,而是通过代理模式创建代理对象,等到使用对象的相关信息时再去查询数据库,从而实现延迟加载。这样可以减少不必要的数据库操作和内存占用,提高程序性能和运行效率。当需要使用延迟加载时,可以使用相关注解或配置来指定需要延迟加载的属性或对象。

什么是 mybatis中的一级缓存和二级缓存?

Mybatis的一级缓存和二级缓存是两个不同的缓存机制。

一级缓存是在同一个session内共享的缓存,即在同一个session中,如果多次查询同一个对象,第一次查询数据会放入缓存中,在后续的查询中可以直接从缓存中取出数据,避免了多次查询的开销。一级缓存是默认开启的,但可以通过 session.clearCache() 方法手动清空。

二级缓存是sessionFactory级别的缓存,需要配置才会开启。当进行sql语句查询时,先查看一级缓存,如果不存在,访问二级缓存,降低数据库访问压力。

mybatis 中的执行器(Executor)?

MyBatis有三种执行器(Executor):

  1. SimpleExecutor:每执行一个请求(statement),就开启一个新的Statement对象,执行完后关闭Statement,适用于小型应用或简单查询场景。

  2. ReuseExecutor:复用一个Statement对象,尽可能避免每次执行请求(statement)时开启新的Statement对象。适用于大型应用或复杂查询场景。

  3. BatchExecutor:批量执行Statement对象,将多条SQL语句组合在一起执行,适用于大型数据集操作场景,可以显著提高性能。

mybatis 和 hibernate 的区别有哪些?

MyBatis和Hibernate是两种不同的ORM框架,它们的区别主要在以下几点:

  1. 映射方式不同:MyBatis使用XML文件或注解配置SQL语句和结果集映射,而Hibernate使用注解或XML定义实体类与数据库表之间的映射关系。

  2. SQL控制方式不同:MyBatis完成SQL语句的映射之后,需要手动编写Java代码进行调用;而Hibernate则是通过面向对象的方式对数据库进行操作,不需要直接编写SQL语句。

  3. 性能不同:MyBatis在处理大量数据的情况下,性能更优,因为SQL语句是预编译并缓存的,可以重复利用,而Hibernate会自动生成的SQL语句效率可能会不如手写的SQL语句。

  4. 灵活度不同:MyBatis可以复用已有的SQL语句以及数据库存储过程,而Hibernate则需要面向对象的方式编写SQL语句,可能不能与已有的存储过程进行兼容。

myBatis如何查询多个id,myBatis常用属性有哪些?

Page<UserPoJo>  getUserListByIds(@Param("ids") List<Integer> ids);
<select id="getUserListByIds" resultType="com.guor.UserPoJo">
    select * from student
    where id in
    <foreach collection="ids" item="userid" open="(" close=")" separator=",">
        #{
    
    userid}
    </foreach>
</select>

mybatis如何防止sql注入

MyBatis的参数处理机制自带防止SQL注入的功能。MyBatis在处理参数时,会将传入的参数值转换成对应的Java类型,使得传入的参数值无法成为SQL语句的一部分,从而防止SQL注入。

此外,还可以使用#{}来替代传统的SQL拼接,#{}会自动进行参数解析,并且防止SQL注入。在使用#{}时,MyBatis会将SQL语句中的#{}占位符替换成带有问号的占位符,然后在执行SQL语句时,将实际参数值安全地设置到这些占位符中。

hibernate 中如何在控制台查看打印的 sql 语句?

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

hibernate 有几种查询方式?

Hibernate 有5种查询方式:

  1. HQL (Hibernate Query Language)
  2. Criteria API
  3. Native SQL
  4. Query By Example (QBE)
  5. Named Queries

hibernate 实体类可以被定义为 final 吗?

不推荐将 Hibernate 实体类定义为 final,因为 Hibernate 需要使用动态代理机制来生成子类,以便实现延迟加载和其他功能。如果将实体类定义为 final,则会阻止 Hibernate 生成子类,从而导致这些功能无法正常工作。此外,如果将实体类定义为 final,还会对测试和扩展造成一定的影响。因此,通常不建议将 Hibernate 实体类定义为 final

在 hibernate 中使用 Integer 和 int 做映射有什么区别?

在 Hibernate 中使用 Integer 和 int 进行映射时存在以下区别:

  1. Integer 是一个对象,而 int 是一个基本数据类型。
  2. 当使用 Integer 进行映射时,可以使用 null 值,而 int 不可以。
  3. 如果使用 int 进行映射,在数据库中字段也必须为 int 类型,而使用 Integer 进行映射时,可以使用 integer 类型的数据库字段。

因此,如果需要使用 null 值,则应该使用 Integer 进行映射,如果不使用 null 值,则可以使用 int 进行映射。

介绍一下 Spring Boot?Spring Boot 的优点有哪些?

Spring Boot是一个基于Spring框架的开源框架,它可以用最少的配置帮助开发者快速开发、部署、运行Spring应用程序。Spring Boot采用众多优秀的开源技术和第三方工具,提供了许多开箱即用的功能,如:自动配置、轻量级、快速开发和部署、微服务支持等,使得Spring应用程序的开发变得更容易、更快捷、更高效。

Spring Boot的优点主要有以下几个:

  1. 简化开发:Spring Boot提供了自动配置机制,可以根据项目的依赖和配置信息,自动配置Spring应用程序的环境,开发者无需手动进行繁琐的配置。

  2. 提高效率:Spring Boot提供了一系列开箱即用的功能,如Web开发、数据库操作、消息传递、安全管理、监控等,可以大大减少开发人员的开发时间和精力。

  3. 微服务支持:Spring Boot提供了丰富的支持,可以轻松地搭建和维护微服务架构,让开发者更加专注于业务逻辑的实现。

  4. 易于部署:Spring Boot应用程序启动时只需执行一条命令即可,无需手动部署多个文件,极大地简化了部署流程。

  5. 社区支持:Spring Boot有庞大的社区支持,提供了大量的文档、教程和样例代码,开发者可以轻松地获得帮助和分享经验。

什么是Spring Boot 中的监视器?

Spring Boot 的监视器是一种检测和监控应用程序的工具,它可以提供实时的应用程序指标和度量信息。Spring Boot 自带了一个监视器模块,可以通过配置和集成第三方库来使用。监视器可以提供以下信息: 应用程序的健康状况、请求的处理情况、内存使用情况、HTTP请求的统计等。监视器可以通过HTTP端点、JMX、日志和其他方式提供信息。

说一下YAML?

YAML(YAML Ain’t Markup Language)是一种轻量级的数据序列化语言,用于表示数据结构以及构建配置文件。它不是一种标记语言,而是一种基于缩进的、可读性强的结构化数据格式。YAML 的语法简洁、易于理解,与多种编程语言兼容,被广泛应用于各种 Web 应用程序、系统配置和数据交换等方面。

说一下Spring Boot 的分页和排序?

Spring Boot 提供了一个叫做 Spring Data 的子项目,其中包含了一个叫做 Spring Data JPA 的库,它可以让我们更方便地操作数据库,并且支持分页和排序。

Spring Boot 实现异常处理?

在 Spring Boot 中,可以使用 @ControllerAdvice 注解和 @ExceptionHandler 注解来实现异常处理。

以下是实现异常处理的步骤:

  1. 创建一个异常处理类,并使用 @ControllerAdvice 注解标注它,这样该类中的 @ExceptionHandler 方法将会处理来自所有 @Controller 中的异常。
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorMessage> handleException(Exception e) {

        ErrorMessage error = new ErrorMessage(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
  1. 在 @ExceptionHandler 注解中定义对特定异常的处理方法,例如:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorMessage> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {

    List<String> errors = e.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.toList());

    ErrorMessage error = new ErrorMessage(HttpStatus.BAD_REQUEST.value(), errors.toString());
    return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

以上方法会处理 MethodArgumentNotValidException 异常,该异常会在请求参数校验失败时被抛出。

  1. 声明一个全局异常捕获过滤器。
  2. 实现 HandlerExceptionResolver 接口处理异常

单点登录

单点登录(Single Sign-On,SSO)是一种身份验证和授权机制,允许用户使用一组凭据一次登录到多个相关但独立的软件应用程序,而无需重新输入认证凭据。通过实现单点登录,用户可以避免记住多组不同的用户名和密码,并且可以更方便地访问多个应用程序。基于标准协议(例如OAuth和OpenID Connect)的单点登录解决方案已经得到广泛的使用,在企业、教育机构和在线服务提供商等多个领域中得到了广泛的应用。

Spring Boot比Spring多哪些注解

Spring Boot基于Spring框架,因此它继承了Spring框架的所有注解,同时还提供了一些自己的注解和功能。

  1. @SpringBootApplication:这是一个标志性注解,用于在Spring Boot应用程序中定义一个主要的配置类。这个注解等价于同时使用@Configuration、@EnableAutoConfiguration和@ComponentScan。

  2. @EnableAutoConfiguration:这个注解启用Spring Boot的自动配置机制。它会根据当前项目的依赖关系,自动配置所需的beans。

  3. @RestController:这个注解用于定义一个RESTful Web服务。与@Controller不同,@RestController包含了@ResponseBody注解,可以将方法返回的数据转换为JSON/XML格式的响应。

  4. @GetMapping、@PostMapping、@PutMapping、@DeleteMapping:这些注解用于定义HTTP请求的方法类型。

  5. @PathVariable:这个注解用于获取URI中的路径参数。

  6. @RequestBody:这个注解用于获取HTTP请求的请求体内容。

  7. @ResponseStatus:这个注解用于指定HTTP响应的状态码。

  8. @ConfigurationProperties:这个注解用于将一些配置属性自动绑定到Bean对象的属性上。

  9. @EnableConfigurationProperties:这个注解用于启用@ConfigurationProperties注解。

  10. @ConditionalOnClass、@ConditionalOnMissingClass、@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnProperty:这些注解用于在特定条件下进行bean的条件化创建。

  11. @Async:这个注解用于将方法定义为异步执行。

除此之外,Spring Boot还提供了一些特定的注解,如@EnableCaching、@EnableRedisRepositories、@EnableEurekaClient等,这些注解可用于启用特定的功能模块。

打包和部署

Spring Boot 应用的打包和部署可以采用如下的方式:

  1. 打包

创建项目后,可以使用 mvn package./gradlew build 命令进行打包。打包的输出文件是一个 jar 文件,其中包含了所需的所有依赖和配置文件。在 target 或 build/libs 目录下可以找到该 jar 文件。

  1. 部署

将 jar 文件部署到服务器上可以选择以下几种方式:

  • 直接在服务器上运行 jar 文件。使用命令 java -jar <jar文件名> 即可启动应用,注意要指定正确的路径和文件名。
  • 使用外部的应用服务器,例如 Tomcat 或 Jetty。可以将 jar 文件作为一个 WAR 包部署到外部应用服务器中。
  • 将 jar 文件打包成 Docker 镜像并发布到 Docker 仓库中。可以使用 Docker 的命令运行该容器。

以上三种方式都可以实现 Spring Boot 应用的部署,选择哪种方式取决于具体的部署环境和需求。

Spring Boot如何访问不同的数据库

Spring Boot可以通过设置数据源来访问不同的数据库。以下是访问MySQL和MongoDB数据库的示例:

  1. MySQL

1.1 添加MySQL依赖

在pom.xml文件中添加以下依赖:

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- ... -->
</dependencies>

1.2 配置数据源

在application.properties文件中配置MySQL数据源:

spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=123456

1.3 注册数据源

在配置类中注入数据源:

@Configuration
public class DataSourceConfig {
    
    
    @Bean
    @ConfigurationProperties(prefix = 

easyExcel如何实现

package com.zh.oukele.listener;
 
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.zh.oukele.model.ExcelMode;
 
import java.util.ArrayList;
import java.util.List;
 
/***
 *  监听器
 */
public class ExcelModelListener extends AnalysisEventListener<ExcelMode> {
    
    
 
    /**
     * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 5;
    List<ExcelMode> list = new ArrayList<ExcelMode>();
    private static int count = 1;
    @Override
    public void invoke(ExcelMode data, AnalysisContext context) {
    
    
        System.out.println("解析到一条数据:{ "+ data.toString() +" }");
        list.add(data);
        count ++;
        if (list.size() >= BATCH_COUNT) {
    
    
            saveData( count );
            list.clear();
        }
    }
 
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
    
    
        saveData( count );
        System.out.println("所有数据解析完成!");
        System.out.println(" count :" + count);
    }
 
    /**
     * 加上存储数据库
     */
    private void saveData(int count) {
    
    
        System.out.println("{ "+ count +" }条数据,开始存储数据库!" + list.size());
        System.out.println("存储数据库成功!");
    }
 
}

什么是数据库的三范式?

关系数据库的三个正常形式(又称3NF)是以下三个级别的逐步规范化的过程:

  1. 第一范式(1NF):确保每一列都是原子性的,即确保每一列都是不可分割的单一数据项。

  2. 第二范式(2NF):在满足第一范式的基础上,确保表中的每一行都有唯一标识,也就是确保表中的每一行都必须可以被唯一地区分出来。

  3. 第三范式(3NF):在满足第二范式的基础上,保证每一列都与主键直接相关,而不是与其他非主键列相关。换句话说,每一列都必须与主键相关,而不是与其他非主键列相关。这可以避免表中的冗余数据。

如何获取当前数据库版本?

//MySQL,,mysql -v
select version();
//Oracle 
select * from v$version;

ACID 是什么?

ACID是指数据库事务的四个基本特性,即原子性、一致性、隔离性和持久性。

  • 原子性(Atomicity):指事务是一个不可分割的单位,要么全部执行,要么全部回滚,不会出现中间状态。
  • 一致性(Consistency):指事务执行前后,数据库中的数据必须保持一致,即满足业务规则,在事务执行前后,数据库中的数据不会发生异常。
  • 隔离性(Isolation):指在并发环境下,多个事务之间应该相互隔离,一个事务的执行不应该影响到其他事务的执行,保证每一事务的执行结果是独立的。
  • 持久性(Durability):指事务提交后,对数据所做的更新操作是永久有效的,即使系统发生宕机等异常情况,数据也不会丢失。

ACID是保证数据库事务的正确性和可靠性的重要特性。

char 和 varchar 的区别是什么?

char 和 varchar 的区别在于它们的存储方式和处理方式不同。

  1. 存储方式:char 类型在数据库中始终占用固定的空间,即使存储的数据长度比其指定长度短,也会占用指定长度的空间。而 varchar 类型则是根据存储的数据长度动态分配空间。

  2. 处理方式:char 类型的数据处理速度较快,因为在查询和排序时不需要考虑字符串长度的变化。而 varchar 类型的处理速度相对较慢,因为在处理字符串长度改变时需要进行额外的计算。

总的来说,如果存储的数据长度不太变化,且占用空间不是很大的情况下,建议使用 char 类型;如果存储的数据长度较不稳定,且需要占用大量空间的情况下,建议使用 varchar 类型。

float 和 double 的区别是什么?

float和double都是C语言中的浮点数据类型,区别在于精度和存储大小。

float类型是单精度浮点型,占4个字节(32位),精度为6-7位小数。

double类型是双精度浮点型,占8个字节(64位),精度为15-16位小数。

因此,如果需要更高的精度,应该使用double类型,但会对内存消耗更大。如果数据量足够小,使用float类型就够了。

Oracle分页sql

Oracle分页SQL可以使用ROW_NUM和子查询来实现。以下是一个查询前10条记录的示例:

SELECT *
FROM (
  SELECT ROW_NUMEBR() OVER (ORDER BY id) AS row_num, id, name, age
  FROM employees
)
WHERE row_num <= 10;

在这个查询中,ROW_NUMBER()函数用于对结果集中的每一行进行编号。然后,使用子查询来选择前10行,其中WHERE子句限制了行数小于或等于10。通过更改WHERE条件,可以选择不同的页数和页大小。

数据库如何保证主键唯一性

  1. 定义主键为唯一标识符:数据库系统允许我们定义唯一标识符主键。这样,当尝试在数据库中插入已存在的主键时,数据库系统就会拒绝该操作。

  2. 数据库索引:数据库索引是一种数据结构,它可以帮助数据库系统快速查找数据。当我们将主键字段设为索引时,它会在每次插入操作之前自动检查是否已存在重复记录。

  3. 数据库约束:在数据库中,我们可以定义约束来限制数据表中的数据。例如,我们可以将唯一约束(UNIQUE)放到主键字段上,这样每次插入操作时,它都会检查是否已经存在相同的值。

  4. 应用程序层面的唯一性检查:应用程序可以定期或在每次插入操作前检查是否已经插入了具有相同主键值的记录,从而可以实现数据的校验和唯一性。

  5. 自动递增主键:使用自动递增主键可以确保每次插入操作时都能够自动生成不同的主键值,从而避免数据重复的问题。

如何设计数据库

数据库设计是一个复杂的过程,需要考虑多个因素。以下是一些基本步骤:

  1. 确定需求:了解系统的需求和应用场景,这将有助于定义数据集。

  2. 创建ER图:ER图是数据库设计的基础。它可以帮助您确定实体、关系和属性。

  3. 设计范式:范式可以帮助您规范化数据库。规范的数据库更容易修改、扩展和维护。

  4. 定义外键和索引:外键和索引可以提高查询效率,并保证数据的完整性。

  5. 优化性能:数据量增加后,性能可能会下降。因此,需要考虑性能调优。

  6. 建立安全措施:确保数据库的安全性和可靠性。这包括权限管理和备份策略。

  7. 实施和测试:在生产环境下实施和测试数据库,确保它可以满足需求,并且稳定可靠。

总之,设计数据库需要结合需求、范式、外键和索引、性能、安全、实施和测试等多个方面进行考虑。

性别是否适合做索引

区分度不高的字段不适合做索引,因为索引页是需要有开销的,需要存储的,不过这类字段可以做联合索引的一部分。

如何查询重

要查询重复的数据,可以使用SQL语句中的GROUP BY和HAVING关键字。具体操作如下:

  1. 首先,使用SELECT语句选择要查询的数据列,如:SELECT column1, column2, … FROM table_name;

  2. 然后,使用GROUP BY语句将数据按照指定列进行分组,如:GROUP BY column1, column2, …;

  3. 最后,使用HAVING语句过滤出重复的数据,如:HAVING COUNT(*) > 1;

完整的查询语句如下:SELECT column1, column2, …, COUNT() FROM table_name GROUP BY column1, column2, … HAVING COUNT() > 1;

这样就可以查询到指定列中重复的数据。

数据库优化方法?

数据库优化一般包含以下几个方面:

  1. 数据库设计优化:合理的表结构设计和索引设计可以大幅提高数据库的性能。

  2. SQL语句优化:通过优化SQL语句,如合适的查询条件、索引使用、避免全表扫描等,可以减少查询时间。

  3. 硬件调优:如增加内存、CPU等硬件设备,提升整体性能。

  4. 数据库服务器参数优化:通过修改数据库服务器参数,如缓存大小、连接数等,可以提高数据库性能。

  5. 数据库分区和分片:将大型数据库分成多个分区和分片,以减少查询所需的时间。

  6. 数据库缓存:使用缓存技术,如Memcached等,可以将数据存储在缓存中,以提高数据库查询速度。

  7. 数据库备份与恢复:建立完善的数据库备份与恢复策略,可以避免数据丢失和系统崩溃等问题,提高数据库的稳定性。

索引怎么定义,分哪几种

索引是数据库管理系统中用来加快数据检索速度的一种数据结构。按照定义方式和作用范围的不同,索引可分为以下几种:

  1. B-树索引:基于B-树数据结构定义,可以在数据量较大的情况下快速定位目标数据。

  2. 哈希索引:基于哈希表数据结构定义,适用于对数据进行精确匹配查询。

  3. 聚集索引:用于主键或唯一约束列,按照该列的值对数据进行排序和组织,可使得查询速度更快。

  4. 非聚集索引:对非主键的列进行排序和组织,可以帮助查询更快地定位目标数据。

  5. 全文索引:用来对文本数据进行搜索和匹配的索引,能够加快文本数据的搜索速度。

  6. 空间索引:用于地理信息系统中,对地理位置进行搜索的索引,可以实现基于位置的查询操作。

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

内连接(INNER JOIN):只返回两个表中共同存在的行,即只返回两个表的交集,没有匹配的行不会出现在结果集中。

左连接(LEFT JOIN):返回左表中的所有行,以及与右表中匹配的行,如果右表中没有匹配的行,则返回 null 值。

右连接(RIGHT JOIN):返回右表中的所有行,以及与左表中匹配的行,如果左表中没有匹配的行,则返回 null 值。

可以看出,内连接只返回两个表中共同存在的部分,而左连接则返回左表中所有行,以及右表中匹配的行,右连接则返回右表中所有行,以及左表中匹配的行。

RabbitMQ的使用场景?

RabbitMQ可以用在很多场景中,包括:

  1. 异步任务处理:将耗时的任务异步执行,并返回给客户端通知。

  2. 发布/订阅模式:多个消费者监听同一个队列,按照订阅策略接收消息。

  3. 路由模式:根据指定的路由规则将消息发送到对应的队列中。

  4. 工作队列模式:将任务分发给多个消费者,均衡负载。

  5. RPC模式:客户端向服务器发送调用请求,服务器收到请求后执行相应的操作并返回结果。

总之,RabbitMQ适合于需要高度可靠性、高并发处理、复杂业务场景的应用程序。

RabbitMQ有哪些重要的角色?有哪些重要的组件?

RabbitMQ有以下几个重要的角色:

  1. 生产者(Producer):向RabbitMQ发送消息的应用程序。
  2. 消费者(Consumer):从RabbitMQ接收消息的应用程序。
  3. 代理(Broker):即RabbitMQ服务器,它接收、存储和路由消息,保证消息的可靠传输。
  4. 队列(Queue):消息的缓存区,它存储着等待被消费的消息。

RabbitMQ的重要组件包括:

  1. AMQP协议:即Advanced Message Queuing Protocol,它是RabbitMQ的核心协议,规定了消息的格式和交换方式。
  2. Exchange:消息的分发机制,负责接收生产者发送的消息,并将消息路由到队列。Exchange有三种类型:direct,topic,fanout。
  3. Binding:连接Exchange和Queue的关系。
  4. Virtual host:一个独立的消息环境,每个Virtual host都有自己的Exchange、Queue和绑定关系。
  5. Connection:连接RabbitMQ服务器的客户端与服务端的TCP连接。
  6. Channel:在一个Connection上打开的虚拟连接,一条Channel上只能进行一种操作(发送或接收消息)。
  7. Message:消息的基本单位,包括消息的标识符、消息内容和消息属性等。

RabbitMQ中 vhost 的作用是什么?

RabbitMQ中vhost的作用是将RabbitMQ broker中的逻辑引擎和逻辑实体划分为多个虚拟的组,从而实现对不同应用程序之间的隔离和保护。每个vhost都是一个独立的命名空间,包含一组消息队列、交换机和绑定规则等AMQP实体,可以用来组织和管理RabbitMQ broker中的消息队列和消息传递流程。在vhost中,使用者和生产者可以创建和访问自己的exchange、queue、binding等AMQP实体,并以接收和发布消息的方式来进行应用程序之间的通信。同时,vhost也提供了一定的安全机制,可以通过控制用户和权限来限制用户访问某个vhost中的AMQP实体,从而确保消息队列的安全性和可靠性。

Jvm 的主要组成部分?及其作用?

JVM(Java Virtual Machine)的主要组成部分包括:

  1. 类加载器(ClassLoader):负责将 .class 文件加载到 JVM 内存中,并生成对应的类。类加载器按照特定的顺序、层次结构来寻找和加载需要的类。

  2. 运行时数据区(Runtime Data Area):是 JVM 在运行时需要使用的数据结构的区域,包括了堆、栈、方法区等。不同的线程之间共享方法区和堆内存,每个线程都拥有自己的操作栈(虚拟机栈)。

  3. 执行引擎(Execution Engine):是 JVM 的核心组件,负责执行编译后的字节码。它将字节码解释成具体的操作,并在 JVM 内执行这些操作。执行引擎有两种实现方式:解释器和即时编译器。

  4. 垃圾回收器(Garbage Collector):负责垃圾收集和对象的释放,保证了 JVM 内存的管理和使用效率。JVM 的垃圾回收采用的是自动垃圾回收技术,能够自动释放不再被引用的对象所占用的内存空间。

JVM 的主要作用就是使 Java 程序能够跨平台运行,它提供了一套独立于硬件和操作系统的虚拟化执行环境。JVM 运行时会解释字节码文件,将其转换成当前计算机可识别的机器码并执行。通过 JVM,Java 程序可以在各种硬件平台和操作系统下运行,而无需针对不同的平台编写不同的代码,这大大提升了 Java 开发的效率。

jvm 运行时数据区?

JVM(Java虚拟机)运行时数据区是JVM中的内存区域,主要分为以下几个部分:

  1. 程序计数器(Program Counter Register):一个小的内存区域,用于指示JVM正在执行的字节码指令的位置。

  2. Java虚拟机栈(Java Virtual Machine Stacks):Java线程执行代码时,每个线程都会有一个私有的栈,用于存储操作数栈、方法出口等数据。

  3. 堆(Heap):Java对象被分配在堆上,堆是JVM中最大的内存区域。堆是所有线程共享的。

  4. 方法区(Method Area):存储类信息、常量池、字段描述等元数据信息,也是所有线程共享的内存区域。

  5. 运行时常量池(Runtime Constant Pool):将每个类或接口的常量池信息放入方法区后,在类或接口的每个实例对象和数组对象里面都会有一个对应的运行时常量池。

  6. 本地方法栈(Native Method Stack):用于执行本地方法,也是线程私有的。

什么是类加载器,类加载器有哪些?

类加载器是Java虚拟机(JVM)的一个组成部分,用于在运行时动态加载类的字节码。Java程序由一组类组成,这些类的代码通常被存储在不同的位置,如本地文件系统、网络上的HTTP服务器、数据库中等。类加载器负责将这些散布的类文件加载到JVM中,使得程序能够运行。

Java中的类加载器可以分为以下几种:

  1. 引导类加载器(Bootstrap Class Loader):它是JVM的内置类加载器,用来加载Java类库中的核心类,如java.lang和java.util包中的类。

  2. 扩展类加载器(Extension Class Loader):它是用来加载Java扩展类库中的类,这些类库位于<JAVA_HOME>\lib\ext目录下。

  3. 系统类加载器(System Class Loader):也称应用程序类加载器,它是用来加载应用程序classpath路径下指定的类。

  4. 用户自定义类加载器:如果系统提供的类加载器无法满足需求,可以自定义类加载器。自定义类加载器需要继承ClassLoader类,实现自己的findClass()方法,并将要加载的字节码传递给defineClass()方法。

这些类加载器按照父子关系形成了一个树形结构,称为类加载器层次结构。每个类加载器都有一个父类加载器,除了引导类加载器,它没有父类加载器。当一个类被加载时,先由当前类加载器查找类,如果找不到则委托给父类加载器查找,直到找到或者无法找到为止。如果所有父类加载器都无法找到类,则由当前加载器负责加载该类。

类加载的执行过程?

Java虚拟机将类加载分为以下五个过程:

  1. 加载(Loading):(类加载器)通过类的全限定名来读取类的二进制字节流,并将其转化为方法区中的运行时数据结构。

  2. 验证(Verification):主要目的是保证类文件的字节流包含的信息符合Java虚拟机规范的要求,并不会危害虚拟机的自身安全。

  3. 准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值。比如说,一个static int a = 0声明,会在准备阶段分配两个字节给a,并初始化为0。

  4. 解析(Resolution):将常量池中的符号引用转化为直接引用的过程,这里说的直接引用就是内存地址。比如说,类A在常量池中引用了类B,则在解析阶段会把类B的引用转化为类B的内存地址。

  5. 初始化(Initialization):是类加载的最后一步,也是执行类构造器()的过程。在这个阶段,虚拟机会执行程序员编写的类初始化代码,虚拟机保证类的()方法在多线程环境下被正确加锁和同步。

注意,这个过程是一个递归的过程。比如说,如果类A继承了类B,那么在加载类A时,会先加载类B,然后才会加载类A。同时,子类在初始化之前会有父类的初始化,若父类没有进行初始化,会首先触发执行父类的初始化。

JVM的类加载机制?

JVM的类加载机制主要分为三个步骤:

  1. 加载(Loading):将需要被执行的类的二进制代码加载到JVM中,并生成对应的Class对象。

  2. 链接(Linking):将类的二进制代码转化为JVM可用的运行状态。包括以下三个步骤:

    (1)验证(Verification):验证被加载的类是否符合JVM规范,包括字节码的结构、语法、类与类之间的引用是否正确等。

    (2)准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值。

    (3)解析(Resolution):将类中的符号引用转换为直接引用。

  3. 初始化(Initialization):在JVM内部执行类的初始化,包括为类的所有静态变量赋予正确的初始值,执行静态代码块等。

注意:类的加载只会执行一次,也就是说,当第一次加载某个类时,JVM就会将其加载、链接、初始化完毕并生成对应的Class对象,以后再次加载同一个类时,就直接使用之前加载的Class对象,不会再重新加载。

什么是双亲委派模型?

双亲委派模型是Java语言中的一种类加载机制。根据该模型,当一个类被加载到JVM中时,JVM会首先检查该类是否被当前类加载器加载过,如果没有则会向其父类加载器发起请求,请求父类加载器尝试加载该类。如果父类加载器无法找到该类,则会向更上层的父类加载器发起请求,直到最终到达顶层的Bootstrap ClassLoader为止。顶层的Bootstrap ClassLoader则是由JVM虚拟机实现的,并且用于加载核心类库。

这种类加载机制保证了类的唯一性,避免了多个不同的类加载器加载同一个类的情况出现,使得Java程序能够更加稳定、可靠。同时,该模型还保证了类的安全性,因为只有处于特定的类加载器层次结构的类才能够相互访问。

如何判断对象是否可以被回收?

  1. 引用计数法:当对象被创建时,给该对象一个引用计数器,每被一个变量引用,计数器就+1,每个引用它的变量释放或者指向其他对象时,计数器就-1。计数器为0时,对象就可以被回收。缺点是会出现循环引用的问题。
  2. 可达性分析算法:从一些外部对象(如根对象)开始,找到所有从这些对象能到达的对象,如果某个对象无法被找到,则认为它是不可达的,可以被回收。缺点是需要扫描整个对象图,如果对象图很大,则开销很大。
  3. 弱引用:弱引用是一种特殊的引用方式,可以引用一个对象但无法增加对象的计数器,当一个对象只被弱引用所引用时,该对象就可以被回收了。
  4. 终结器(finalize):Java提供了finalize()方法,用于回收对象前的处理工作,对象直到finalize()方法执行后才能被垃圾回收器所回收。缺点是finalize()方法的执行存在不稳定性,不能保证一定执行。

Jvm 都有哪些垃圾回收算法?

JVM 的垃圾回收算法可以分为以下几种:

  1. 标记-清除算法(Mark and Sweep):先标记不再使用的对象,再清除这些对象,但该算法会产生空间碎片,对后续的分配可能会产生影响。

  2. 复制算法(Copying): 将堆内存划分为两块区域,每次只使用其中一块,当需要进行垃圾回收时,将当前正在使用的区域中的存活对象复制到未使用的区域中,然后清除当前正在使用的区域,使其变为未使用的区域。这种算法高效,但是需要额外的空间。

  3. 标记-整理算法(Mark and Compact):先标记不再使用的对象,再将存活对象向一端移动,最后清除边界之外的所有对象,这种算法会整理空间,减少空间碎片,但效率较低。

  4. 分代算法(Generational):将堆内存划分为新生代和老年代两个区域,新生代中的大部分都是短生命周期的对象,老年代中的对象生命周期较长。针对不同区域进行不同的垃圾回收策略使得效率更高。

  5. G1算法:是一种基于分代算法和复制算法的混合垃圾回收算法,将堆内存分为多个区域,根据使用情况进行分区和垃圾回收,可以有效减少内存占用和回收时间。

不同的算法在不同的场景下有着各自的优缺点,需要根据具体情况进行选择。

Jvm 的些垃圾回收器?

Java虚拟机(JVM)包含多种垃圾回收器,主要为以下几种:

  1. Serial 垃圾回收器:串行垃圾回收器是最简单的一种垃圾回收器,它是单线程的,适合小型应用环境。

  2. Parallel 垃圾回收器:并行垃圾回收器使用多个线程一起执行垃圾回收任务,它适用于大型系统中的多核处理器。

  3. CMS 垃圾回收器:CMS垃圾回收器(Concurrent Mark Sweep)是一种并发垃圾回收器,它在运行时不会暂停应用程序,并且适用于响应时间要求比较高的场景。

  4. G1 垃圾回收器:G1垃圾回收器(Garbage-First)是一种面向大型内存应用的垃圾回收器,它能够优化内存分配和回收,并能够动态监视和调整内存分配策略。

  5. ZGC 垃圾回收器:ZGC垃圾回收器(Z Garbage Collector)是JDK 11中新增的一种实验性低延迟的垃圾回收器,它从整个堆上进行垃圾回收,具有非常低的暂停时间,适用于高性能、低延迟的大型应用程序。

以上是主要的垃圾回收器,具体选择哪种垃圾回收器,需要根据不同应用程序的情况进行选择。

JVM栈堆概念,何时销毁对象

JVM中的堆是用于存储对象的内存区域,而栈是用于存储线程执行时的局部变量、方法调用和返回值的内存区域。

在JVM中,当一个对象不再被引用时,它就可以被GC回收,GC会遍历所有的对象,标记哪些对象是无法被访问和使用的,然后释放这些对象占用的内存空间。

在JVM中,当一个方法执行结束时,其在栈中分配的局部变量和方法参数就会被销毁。此外,当JVM检测到没有指向堆中某个对象的引用时,它就会把这个对象回收掉。因此,JVM的垃圾回收机制会自动销毁对象。

介绍一下新生代垃圾回收器和老生代垃圾回收器?都有什么区别?

常见的新生代垃圾回收器有两种:复制算法和标记-压缩算法。常见的老生代垃圾回收器有三种:标记-清除算法、标记-整理算法和增量标记-整理算法。

新生代垃圾回收器和老生代垃圾回收器的区别主要体现在垃圾回收的对象和机制上。新生代垃圾回收器主要回收短命对象,使用复制算法或标记-压缩算法,将存活的对象复制到另一个空间,再清除垃圾。老生代垃圾回收器主要回收长生命周期对象,使用标记-清除算法、标记-整理算法或增量标记-整理算法,标记需要回收的对象,然后对它们进行清理或压缩。老生代垃圾回收器的效率相对较低,但是能够有效解决内存碎片问题。

什么是CMS 垃圾回收器?

CMS(Concurrent Mark and Sweep)垃圾回收器,是一种面向服务端应用的低延迟垃圾回收器。与传统的垃圾回收器不同的是,CMS 垃圾回收器在不中断正在运行的应用程序的情况下,对内存中的垃圾进行回收。这种垃圾回收方式可以有效地减少应用程序的停顿时间,提高应用程序的响应性能。

CMS 垃圾回收器的核心机制是使用并发标记-清除算法。该算法通过在应用程序并发运行的同时,在另一个线程中执行垃圾回收操作。具体来说,CMS 垃圾回收器将垃圾回收过程分为两个阶段:标记阶段和清除阶段。

在标记阶段,CMS 垃圾回收器使用多个线程来标记出当前不再使用的对象。这个过程中,应用程序继续运行,同时垃圾回收器在不影响应用程序运行的情况下,标记出所有的垃圾对象。标记完成后,进入清除阶段。

在清除阶段,CMS 垃圾回收器回收分配给垃圾对象的内存空间。与传统的垃圾回收器不同的是,CMS 垃圾回收器不需要等待所有对象都被标记后再进行清除。相反,它每次只清理一小部分空间,然后释放该空间。这种方式可以避免在清理时线程卡顿的情况发生,从而提高应用程序的响应速度。

总结一下,CMS 垃圾回收器是一种低延迟的垃圾回收器,它通过并发标记-清除算法,在不影响应用程序运行的情况下,尽可能快地回收内存中的垃圾对象。它可以显著提高应用程序的响应性能,适用于对停顿时间要求比较高的服务端应用。

说一下分代垃圾回收器是怎么工作的?

分代垃圾回收器是一种常见的垃圾回收技术,它将堆内存分为三个代:新生代(Young Generation)、老年代(Tenured Generation)和永久代(Permanent Generation,JDK7以后已经被移除)。新生代存放的是新创建的对象,而老年代存放的是已经多次使用的对象。

具体的工作流程如下:

  1. 当程序申请内存时,分代器将内存分配给新生代。新生代在物理上被分为两部分:Eden区和两个Survivor区,一般是8:1:1的比例。Eden区是所有新对象被创建时的初始位置,Survivor区作为缓冲区,存放从Eden区中幸存下来的对象。

  2. 当新生代的内存满了,就会触发Minor GC(Minor Garbage Collection),也称为Young GC。这时垃圾回收器会扫描Eden区和Survivor区,并将不再被使用的对象进行回收。幸存的对象会被移到Survivor区的另一侧,直到Survivor区满为止,再一起移到老年代。

  3. 在老年代中,由于对象生命周期较长,垃圾回收比较困难,所以触发老年代的垃圾回收需要条件相对严格。当老年代的内存空间满了,就会触发Full GC,垃圾回收器会对整个堆内存进行扫描,清除不再被使用的对象。

总的来说,分代垃圾回收器通过将内存分为不同代降低了垃圾回收的成本,提高了Java虚拟机的垃圾回收效率。

什么是Redis?

Redis是一个开源的内存键值存储系统,支持多种数据结构,包括字符串、哈希表、列表、集合、有序集合等。它可以用作数据库、缓存和消息中间件等多种用途。Redis的特点包括高速、可扩展、持久化、支持事务和 Lua 脚本等。Redis常用于构建高并发的Web服务,也是很多大型网站和应用程序的核心技术之一。

Redis常见使用场景?

  1. 缓存:Redis 作为高性能缓存数据库使用,可用于数据缓存、页面缓存、会话管理等。
  2. 计数器:Redis 适合实现数据计数器,如用户行为计数、在线用户数统计等。
  3. 队列和消息通信:Redis 可以作为轻量级消息代理,用于实现任务队列、发布/订阅模型等。
  4. 分布式锁:Redis 支持分布式锁,可用于实现分布式环境中的排他资源访问和并发控制。
  5. 地理位置定位:Redis 通过其支持的 Geohashing 算法可以实现基于地理位置的搜索和定位功能。
  6. 排行榜:Redis 支持排序功能,可用于实现各种类型的排行榜。
  7. 实时应用数据分析:Redis 支持数据持久化,可用于实时应用的数据统计和分析。
  8. 限流:Redis 可用于实现限流机制,防止系统被恶意攻击或非法占用资源。

Redis有哪些功能?

Redis有以下功能:

  1. 数据库:Redis是一个用于存储和管理数据的数据库系统。

  2. 缓存:Redis可以将数据存储在内存中,以提高系统的读取和写入速度。

  3. 发布/订阅系统:Redis提供消息传递功能,可以让一个系统发布消息,而其他系统可以订阅这些消息。

  4. 计数器:Redis提供了原子操作来实现计数器功能。

  5. 分布式锁:Redis提供了分布式锁功能,可以用来实现分布式系统中的协调。

  6. 数据结构处理:Redis支持字符串、散列、列表、集合和有序集合等数据结构,可以用于处理各种不同的数据。

  7. 数据落地:Redis可以将数据存储到磁盘中,以便重启时能够重新加载数据。

Redis都支持哪些数据类型?

  1. String:一个字符串类型的数据结构,存储最基础的数据类型,如常量、变量和 JSON 数据等。
  2. List:一个链表类型的数据结构,类似于数组,但是支持更多的功能,如:向头尾插入,删除等。
  3. Hash:一种键值对型的数据结构,可以存储多个键值对,每个键对应一个值。
  4. Set:一个无序集合类型的数据结构,不允许有重复的元素,支持集合的基本操作,如并集、交集和差集等。
  5. Sorted Set:一个有序集合类型的数据结构,每个元素都有一个分数,根据分数排序,支持集合的基本操作,如并集、交集和差集等。

Redis取值和存值

jedis.set("key","value");
jedis.get("key");
jedis.del("key");
//给一个key叠加value
jedis.append("key","value2");//此时key的值就是value + value2;
//同时给多个key进行赋值:
jedis.mset("key1","value1","key2","value2");

为什么Redis是单线程的?

  1. 减少多线程并发带来的线程切换开销,提高系统的性能。

  2. Redis是基于内存的数据库,其处理速度非常快,大部分操作的瓶颈都在于CPU的计算和内存的访问速度,而不是上下文切换。因此单线程的方式可以充分利用现代CPU的多级缓存,充分利用CPU的cache机制。

  3. 单线程可以避免并发访问导致的数据一致性问题,使得Redis可以充分利用CPU运算单元,从而更好地发挥性能。

  4. Redis的单线程模型也有极其优秀的异步IO设计,可以充分利用现代系统的多核CPU,提高系统的吞吐量。

因此,Redis采用单线程模型是为了充分利用系统资源,提高性能和稳定性。但是需要注意的是,这也意味着在处理大量并发请求时,Redis的性能可能会受到影响。

Redis6.0之前是单线程的,Redis6.0之后开始支持多线程:

在Redis 6.0中,引入了多线程运行模式,称为Redis-Threads。它利用了现代CPU的多核性能,可以同时处理多个客户端请求,大大提高了Redis的处理能力和吞吐量。

Redis-Threads模式的架构是通过将Redis实例的工作负载分配到多个独立的线程中,让Redis能够“同时”处理多个请求。每个线程都有自己的事件循环,Redis的事件驱动模型保证了请求的并发处理流程。

然而,需要注意的是,Redis-Threads模式并不是完全的无阻塞模式,它仍然存在一些阻塞点。例如,线程之间会进行锁的协调和互斥操作,这些操作会导致一定程度上的阻塞。但Redis-Threads模式相比传统的单线程模式,整体上提供了更高的吞吐量和更好的响应时间。

说一下Redis持久化?

Redis持久化有两种方式:RDB快照和AOF日志。

  1. RDB快照:将Redis在内存中的数据生成快照并存储到磁盘上。可以定期执行快照备份,也可以手动执行备份。在Redis重新启动时,可以使用快照文件还原数据。RDB对磁盘空间的利用率比较高,适合数据量大、数据变化不频繁的场景。

  2. AOF日志:将Redis的操作记录追加到文件中,并通过fsync函数将日志写入磁盘。不断追加的操作日志可以精确地重新构建Redis当前状态。AOF日志支持三种追加模式:每秒钟一次、每个命令一次、安全模式。适合数据变化频繁、需要快速恢复的场景。

Redis和 memecache 的区别是什么?

Redis 和 memcached 都是内存缓存系统,它们都提供提高访问速度的存储介质,但是它们也有一些不同之处:

  1. 数据结构:Redis 支持更多的数据结构,包括字符串、列表、哈希表、集合、有序集(Sorted Set)和 HyperLogLog 等,而 memcached 只支持键值对。

  2. 持久化:Redis 支持数据持久化,它可以将数据保存到磁盘上,以便在服务重启时数据不会丢失。Memcached 不支持数据持久化。

  3. 内存管理:Redis 使用虚拟内存技术,并将数据集保存在内存中,当内存用完时,Redis 会将一些不常用的数据写入磁盘,以释放内存。 memcached 使用 LRU 算法来释放内存。

  4. 单线程:Redis 是单线程处理请求的,每个请求都是顺序处理的,而 memcached 是多线程处理请求的。

  5. 效率:适用场景不同。Redis 适用于操作数据较为复杂的场景,比如对数据集进行排序、分析,同时需要高并发支持等。而 memcached 适用于简单的键值对缓存,例如缓存数据查询结果等简单场景。

Redis支持的Java 客户端都有哪些?

jedis 和 redisson 有哪些区别?

Redis支持的java客户端有以下几种:

  1. Jedis:Jedis是一个使用Java语言编写的简单、快速、高性能的Redis客户端库,目前是最流行的Redis java客户端之一。

  2. Lettuce:Lettuce是用Java编写的一款快速、可扩展、异步的Redis客户端,在异步场景下具有更高的性能,并提供多种API来操作Redis。

  3. Redisson:Redisson是一个基于Java的Redis客户端,提供面向对象的API,方便快捷地将Redis作为分布式锁、队列、Map和Set等数据结构的基础组件来使用。

  4. JRedis:JRedis是JVM上的Redis客户端,提供简洁的API、高性能和高可靠性,非常适用于开发Web应用程序和分布式系统。

  5. Redis client for Java:这是一个通过Java实现的Redis客户端,支持Redis的所有操作,包括流水线和事务等操作。它还支持使用自定义序列化器来序列化和反序列化Redis的数据结构。

  6. Redisson Reactive:Redisson Reactive是一个基于Reactive Streams的Redis客户端,提供Reactive API并支持非阻塞I/O操作。它可以与各种流式技术和框架(如Spring Reactor和Project Reactor等)一起使用,可用于开发高性能、低延迟的响应式系统。

缓存穿透是什么?说一下怎么解决?

缓存穿透指的是查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层(如数据库)找不到对应的数据,则不写入缓存,这将导致该查询每次都要到存储层查询,从而增加存储层的负载,甚至可能引起宕机。

解决缓存穿透的方法有:

  1. 布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对下游存储系统的查询压力。

  2. 缓存空对象:在缓存中将不存在的数据也缓存起来,对于不存在的数据,也写入缓存,但数据为空对象(如 null),这样下次缓存中有该数据时,就能够命中缓存,而不必查询存储系统。

  3. 延迟双删:如果从存储层查不到数据,应该将这个“不存在”的结果也缓存起来,但是过一段时间就应该删除这个缓存,这样可以避免在一个时间窗口内高并发地穿透到存储层。

说一下如何保证缓存和数据库数据的一致性?

要保证缓存和数据库数据的一致性,可以使用以下几种方法:

  1. 缓存雪崩:为了防止某个时间点缓存失效,可能会有大量请求同时到数据库中查询数据,导致数据库短时间内无法承受巨大的查询请求并出现宕机的现象,因此可以引入缓存预热机制,提前预先加载缓存,确保缓存中数据的有效期不会同时过期。

  2. 缓存穿透:如果查询的数据在数据库中不存在,而又被大量的请求查询,这时候对缓存压力会很大,可以采用“布隆过滤器”技术代替缓存中没有的数据的空值,避免无效的查询请求对数据库的影响。

  3. 缓存回写策略:为保证缓存和数据库的数据同步,出现更新操作时,可以采用缓存回写策略,即先更新数据库,再将缓存中的数据删除或更新,这样可以保证数据的一致性。

  4. 双写一致性:应用程序在向数据库写入数据的同时,也要向缓存中写入数据,从而保证数据的一致性,这种方式可以采用“数据库异步读写”机制,即先写入数据库中,然后再写入缓存中,确保数据的可靠性。

Redis实现分布式锁?

Redis 提供的分布式锁实现可以分为以下几个步骤:

  1. 使用 Redis 的 SETNX 命令设置锁值:使用 SETNX 命令尝试加锁,若返回值为 1,则表示加锁成功,否则表示锁已被其他客户端占用。

  2. 设置锁的过期时间:为了避免极端情况下锁一直被占用,导致死锁的问题,可以为锁设置一个过期时间。使用 Redis 的 SETEX 命令对锁设置过期时间。

  3. 释放锁:释放锁需要保证原子性,不能出现误删其他客户端创建的锁的情况。可以使用 Lua 脚本在 Redis 中执行删除锁的操作。

Redis分布式锁的缺陷?

Redis分布式锁主要存在以下缺陷:

  1. 准确性问题:Redis分布式锁在某些情况下可能出现不可靠的问题。例如,当设置过期时间的锁在执行期间长时间出现网络延迟、主从同步(Replication)异步等情况时,会出现锁已失效但资源却被多个客户端同时占用的情况。

  2. 性能问题:Redis分布式锁需要向Redis服务器不断发生请求来检查锁状态和释放锁,这会带来一定的性能开销。在高并发场景下,频繁的请求可能会导致Redis服务器过度负载,从而影响系统的稳定性和可用性。

  3. 功能限制:Redis分布式锁只能实现简单的互斥行为,无法支持更复杂的场景,例如自旋锁、读写锁等。

  4. 实现难度:使用Redis分布式锁需要开发人员具备一定的技术能力和经验,且需要谨慎处理锁的过期时间、自旋次数和重试机制等因素。如果开发人员处理不当,容易导致死锁或者活锁等问题。

如何给Redis内存优化?

Redis是一个内存存储数据库,因此内存优化在Redis中是非常重要的。以下是Redis内存优化的一些技巧:

  1. 使用适当的数据结构:Redis支持五种不同的数据结构,包括字符串、哈希、列表、集合和有序集合。选择适当的数据结构可以节省内存空间。

  2. 设置最大内存限制:通过设置最大内存限制,可以防止Redis使用过多内存并导致系统崩溃。

  3. 使用LRU算法:LRU算法可以帮助Redis在内存不足时自动删除最久未使用的键。

  4. 开启压缩功能:开启Redis的压缩功能可以减少内存使用量。

  5. 分批次操作和分片:对于大型数据集,分批次操作和分片可以降低每个Redis实例的内存压力。

  6. 合理设置过期时间:设置过期时间可以在键过期后自动删除键,释放内存空间。

  7. 避免使用大对象:如果Redis需要处理一个非常大的对象,最好将其存储在文件系统中,并在需要时动态加载。

  8. 将Redis用于缓存:将Redis用于缓存可以减少数据库负载,提高性能,并减少内存使用量。

以上是Redis内存优化的一些技巧,可以帮助优化系统性能、减少内存使用量,降低系统崩溃的风险。

猜你喜欢

转载自blog.csdn.net/weixin_43706224/article/details/129633592