Java
什么是B/S架构,C/S架构?Java都有那些开发平台?
- B/S(Browser/Server),浏览器/服务器程序
- C/S(Client/Server),客户端/服务端,桌面应用程序
- JAVA SE:主要用在客户端开发
- JAVA EE:主要用在web应用程序开发
- JAVA ME:主要用在嵌入式应用程序开发
JDK, JRE, JVM?
JDK(Java Development Kit):JDK是Java开发工具包,它是Java开发的完整工具集,包括了Java编译器(javac)、Java虚拟机(JVM)、Java类库等。主要用于Java应用程序的开发,提供了开发、编译、调试和运行Java程序所需的工具。
JRE(Java Runtime Environment):JRE是Java运行时环境,它是Java应用程序执行的环境,包含了Java虚拟机(JVM)和Java类库。用于在计算机上运行已经编译过的Java应用程序,但不包含用于Java开发的工具,如编译器。
JVM(Java Virtual Machine):JVM是Java虚拟机,是一个在物理计算机上模拟运行Java字节码(.class)的虚拟机。负责解释和执行Java字节码,通过不同操作系统上的 JVM 解释为该操作系统的机器指令,使得Java程序能够在不同的平台上实现一次编译,到处运行的跨平台特性。Java语言有哪些特点?
- 简单易学、有丰富的类库
- 面向对象 OOP(Java最重要的特性,让程序耦合度更低,内聚性更高)
类是对象的抽象,对象是类的具体,类是对象的模板,对象是类的实例 - 与平台无关性(JVM是Java跨平台使用的根本)
- 可靠安全
- 支持多线程
一个java类中包含那些内容?
属性、方法、内部类、构造方法、代码块。数据结构?Java的数据结构有那些?Java中有几种数据类型?
数据结构:计算机保存,组织数据的方式
java中数据结构有:1.线性表 2.链表 3.栈 4.队列 5.图 6.树
数据类型有,整形:byte,short,int,long;浮点型:float,double;字符型:char;布尔型:booleanfloat 和 double.
两种用于表示浮点数的数据类型。它们之间的主要区别在于精度和存储大小。- float是一种单精度浮点数数据类型,通常用于需要小数点的计算,但它不如double类型精确。float类型有7位十进制有效数字,并且它的存储大小是32位(4字节)。
1
float myFloat = 3.14f; // 注意 'f'或F 后缀,强制Java编译器将其识别为float类型,否则它会被默认为double
- double是一种双精度浮点数数据类型,比float类型有更高的精度,通常用于需要更精确计算的情况。double类型有15位十进制有效数字,并且它的存储大小是64位(8字节)。
1
double myDouble = 3.141592653589793; // 由于double的精度更高,若无后缀特别指定,Java中的浮点数默认double
- float是一种单精度浮点数数据类型,通常用于需要小数点的计算,但它不如double类型精确。float类型有7位十进制有效数字,并且它的存储大小是32位(4字节)。
如何解决浮点型数据运算出现的误差的问题?float f=3.4;是否正确?
使用Bigdecimal类
进行浮点型数据的运算。
3.4 是双精度数(double),将 double 赋值给 float 属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换 float f =(float)3.4; 或 float f =3.4Fshort s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1; 有错吗?
对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
而 short s1 =1; s1 += 1;可正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。int i = 0; count = (i++)+ (i++)+ (i++); 值为?
count = 0 + 1 + 2 = 3什么是隐式转换,什么是显式转换?Char类型能不能转成int类型,string类型,double类型?
显示转换就是类型强转,把一个大类型的数据强制赋值给小类型的数据;隐式转换就是大范围的变量能够接受小范围的数据;隐式转换和显式转换其实就是自动类型转换和强制类型转换。
Char < int < long < float < double;Char类型可以隐式转成int,double类型,但是不能隐式转换成string;如果char类型转成byte,short类型的时候,需要强转。char 型变量中能不能存贮一个中文汉字?
可以,因为 Java 中使用的编码是 Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。
补充:使用 Unicode 意味着字符在 JVM 内部和外部有不同的表现形式,在 JVM内部都是 Unicode,当这个字符被从 JVM 内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader 和 OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于 C 程序员来说,要完成这样的编码转换恐怕要依赖于 union(联合体/共用体)共享内存的特征来实现了。数组在java中是一种基本数据类型吗?引用类型?
在Java中,数组(Array)是一种引用类型,而不是基本数据类型。例如,如果你创建数组:int[] numbers = new int[5];
这里,numbers是一个引用变量,指向一个包含5个int类型元素的数组对象。这个数组对象本身不是基本数据类型,而是引用类型。数组中的每个元素(在这种情况下是int类型)是基本数据类型。什么是基本数据类型和引用类型?
- 基本数据类型:是编程语言中内置的简单数据类型,用于直接存储具体的值。这些值通常是不可变的,即一旦赋值,就不能改变它们的类型。基本数据类型通常包括整形:byte,short,int,long;浮点型:float,double;字符型:char;布尔型:boolean。在Java中,基本数据类型是直接存储值的,它们占用的内存空间是固定的,且不会引用其他对象。
- 引用类型:与基本数据类型相对,它们不直接存储值,而是存储对内存中对象的引用。这意味着,引用类型的变量实际上是一个指针,指向存储在堆内存中的对象。当你创建一个引用类型的变量(如数组、类、接口等)时,你实际上是在内存中创建了一个对象,并将变量设置为指向这个对象的引用。引用类型包括类(Class)、接口(Interface)、数组(Array)等。
Java 中变量是“值传递” 还是 “引用传递”?
在Java中,所有的变量都是值传递,无论是基本数据类型还是引用类型。- 基本数据类型的变量存储的是值本身。当你将一个基本数据类型的值赋给另一个变量时,实际上是复制了这个值。所以,对于基本数据类型,Java确实是通过值来传递的。
当一个基本类型变量作为方法的参数传递时,传递的是该变量的一个副本。这意味着在方法内部对该副本所做的任何修改都不会影响到原始变量。基本类型变量是存储在栈内存中的,并且它们的值是直接存储的,而不是通过引用。一旦一个基本类型变量被初始化并存储在栈内存中,你不能直接修改它存储在栈内存中的值。基本类型变量是不可变的(java所有基本类型都不可变?!!),一旦它们被赋值,它们的值就不能被改变。如果想修改其值,必须重新赋一个新的值。这通常是通过声明一个新的变量,或者重新赋值给原来的变量来完成的。1
2
3
4
5
6
7
8
9
10
11public class Main {
public static void main(String[] args) {
int originalValue = 5;
modifyValue(originalValue);
System.out.println(originalValue); // 输出 5,原始值未改变
}
public static void modifyValue(int value) {
value = 10; // 这里修改的是副本的值
System.out.println(value); // 输出 10,副本的值被修改了
}
}1
2
3
4
5
6
7
8
9public class Main {
public static void main(String[] args) {
int number = 5; // number 初始化为 5
System.out.println("Before modification: " + number); // 输出: Before modification: 5
// 修改 number 的值
number = 10; // 重新给 number 赋一个新的值
System.out.println("After modification: " + number); // 输出: After modification: 10
}
} - 引用类型的变量实际上存储的是一个指向内存中对象的引用的地址。当你将一个引用类型的变量赋给另一个变量时,你实际上是在复制这个引用地址,而不是对象本身。这意味着两个变量现在指向同一个对象。因此,尽管在引用类型中,传递的是引用(地址),但这并不等同于传统意义上的“引用传递”。
在Java中,对象本身是通过堆内存来存储的,而引用变量(即对象的引用)是通过栈内存来存储的。当你将一个引用类型的变量赋值给另一个变量时,你实际上是在栈内存中复制了这个引用地址,而不是复制了整个对象。
- 基本数据类型的变量存储的是值本身。当你将一个基本数据类型的值赋给另一个变量时,实际上是复制了这个值。所以,对于基本数据类型,Java确实是通过值来传递的。
Java创建对象有几种方式?
- 使用
new
关键字:1
MyClass obj = new MyClass();
- 通过反射机制:
1
2
3
4
5
6try {
Class<?> clazz = Class.forName("MyClass");
MyClass obj = (MyClass) clazz.newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
} - 使用
clone()
方法:1
2
3
4
5
6MyClass original = new MyClass();
try {
MyClass cloned = (MyClass) original.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
} - 通过反序列化:
1
2
3
4
5
6
7
8// 假设 MyClass 实现了 Serializable 接口
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));
out.writeObject(new MyClass());
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
MyClass obj = (MyClass) in.readObject();
in.close(); - 使用匿名类
- 工厂方法
- 使用静态工厂方法
- 使用
面向对象和面向过程的区别。
- 面向过程:一种较早的编程思想,顾名思义就是该思想是站着过程的角度思考问题,强调的就是功能行为,功能的执行过程,即先后顺序,而每一个功能我们都使用函数(类似于方法)把这些步骤一步一步实现。使用的时候依次调用函数就可以了。
- 面向对象:一种基于面向过程的新编程思想,顾名思义就是该思想是站在对象的角度思考问题,我们把多个功能合理放到不同对象里,强调的是具备某些功能的对象。
具备某种功能的实体,称为对象。面向对象最小的程序单元是类。面向对象更加符合常规的思维方式,稳定性好,可重用性强,易于开发大型软件产品,有良好的可维护性。
在软件工程上,面向对象可以使工程更加模块化,实现更低的耦合和更高的内聚。 - instanceof 关键字的作用
instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。1
boolean result = obj instanceof Class
封装?什么是拆装箱?
Java面向对象语言,一切操作以对象为基础。对象中封装了属性和操作,使用灵活,数据不被外部修改。封装类在处理集合、泛型、反射等场景中非常有用。
装箱就是自动将基本数据类型转换为包装器类型(int->Integer);调用方法:Integer的valueOf(int) 方法拆箱就是自动将包装器类型转换为基本数据类型(Integer->int)。int 与 Integer初始值?
Integer初始值为null,存储在堆内存;int初始值0,存储在栈空间。抽象
抽象是将一类对象的共同特征总结出来构造类的过程, 包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。面向对象的特征有哪些方面?
- 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
- 面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;编写一个类就是对数据和数据操作的封装。
- 优点:
- 信息隐藏: 允许隐藏对象的内部状态和实现细节,只暴露必要的接口。这样可以保护对象的内部数据不被外部直接访问和修改,减少错误和不一致的状态。
- 模块化: 将复杂的系统分解为独立的、可管理的模块。每个模块负责特定的功能,使得开发和维护更加简单。
- 易于维护: 封装使得对象的实现可以独立于其使用者进行更改。只要接口保持不变,内部的修改不会影响到使用该对象的其他部分代码。
- 继承:继承是从已有类得到继承信息创建新类的过程.
- 提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
- 优点:
- 代码重用: 继承允许子类继承父类的属性和方法,减少了代码的重复编写,提高了开发效率。
- 建立层次结构: 继承支持创建一个类的层次结构,这有助于组织和管理具有共同特征的对象。子类可以扩展或覆盖父类的行为,从而实现更具体的功能。
- 多态的基础: 继承是多态实现的基础。通过继承,可使用父类类型的引用来操作子类对象,为多态提供了可能。
- 多态性:是指允许不同子类型的对象对同一消息作出不同的响应。
- 简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。
- 优点:
- 接口统一: 允许不同的子类对象通过统一的接口进行操作,简化了客户端代码的复杂性,可以处理不同类型的对象而不需要知道对象的具体类型。
- 扩展性: 提高了代码的扩展性。如果需要引入新的子类,只需确保它遵循现有的接口,而无需修改已有的代码。
- 动态绑定: 在Java等语言中,多态允许在运行时动态决定对象的具体类型和调用的方法。这种动态绑定提供了更大的灵活性和更强的表达能力。
- 方法重载(overload)实现的是编译时的多态性(也称为前绑定),编译器在编译时根据参数类型和数量确定哪个方法
1
2public int add(int a, int b) return a + b;
public double add(double a, double b) return a + b; - 方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
1、方法重写(子类继承父类并重写父类中已有的或抽象的方法);
2、对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
运行时的多态性主要通过方法重写和对象的多态性实现,运行时根据对象的实际类型来调用相应的方法。这意味着在运行时才能确定调用哪个方法,这取决于对象的实际类型。编译时,Java编译器只知道引用变量的类型,而不知道它所引用的对象的实际类型。因此,方法调用的确定(即分派)被推迟到运行时。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Animal {
public void sound() {
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal {
public void sound() {
System.out.println("The dog barks");
}
}
public class Main {
public static void main(String[] args) {
// 父类引用指向子类对象
Animal animal = new Dog();
// 当调用 animal.sound()时,尽管 animal 是 Animal 类型的引用,
// 但运行时系统会检查 animal 实际引用的对象类型(即Dog)并调用该对象类型(Dog)中的 sound 方法
animal.sound();
}
}
- 总的来说,封装、继承和多态共同为面向对象编程提供了一种强大的方式来构建复杂和可扩展的软件系统。它们使得代码更加清晰、易于维护和扩展,同时也提高了代码的复用性和可读性。极大地提高了软件开发的效率、可维护性和可扩展性。
- 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
重载和重写的区别
- 重写 Override:在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。被重写方法比父类更好访问(即子类函数的访问修饰权限不能少于父类的),不能比父类被重写方法声明更多的异常(里氏代换原则)。
1、发生在父类与子类之间
2、方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
3、访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private),子类重写方法应该要比父类方法具有相同或更广泛的可见性,子类对外暴露的接口比父类更多,这符合面向对象编程中的开闭原则(对扩展开放,对修改关闭),提供了更多的灵活性。
4、重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常 - 重载(Overload)在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
1、重载Overload是一个类中多态性的一种表现
2、重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
3、重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
- 重写 Override:在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。被重写方法比父类更好访问(即子类函数的访问修饰权限不能少于父类的),不能比父类被重写方法声明更多的异常(里氏代换原则)。
动态绑定?
是指在运行时确定方法调用的具体实现。它是 Java 中的一种多态性表现,通过动态绑定,程序可以根据对象的实际类型来决定调用哪个方法实现。
动态绑定的实现依赖于 Java 中的继承和重写机制。当子类重写父类的方法时,如果父类引用指向子类对象,并且调用被重写的方法时,将根据实际对象类型决定调用哪个版本的方法,这就是动态绑定。使得 Java 具有更强大的多态性,可以根据对象的实际类型来调用不同的方法实现,增强了程序的灵活性和可扩展性。public,private,protected,以及不写(默认)时的区别?
在Java中,访问修饰符用于指定类、变量、方法和构造函数的访问权限。它们有四种级别:public、private、protected和默认(无修饰符)。这些修饰符决定了哪些其他类可以访问特定的成员。Java 中,外部类的修饰符只能是 public 或默认,类的成员(包括内部类)的 修饰符可以是以上四种。- public声明类的成员时,它可以在任何其他类中(同一包中/不同的包中)被访问。
- private成员只能在其自己的类中被访问。它不能被任何其他类(即使是同一包中的类)访问。
- protected成员可以在其自己的类、同一包中的其他类以及任何子类(无论子类在哪里)中被访问。
它提供了一种有限的访问,比private更开放,但仍然限制了对类的直接访问。
这在创建API或库时特别有用,因为你可能希望允许子类访问某些方法或变量,但不希望它们被其他不相关的类访问。 - 默认(无修饰符)default:当一个类的成员没有明确的访问修饰符时,它只能在同一包中的其他类中被访问。对于同一个包中的其他类相当于 public,对于不是同一个包中的其他类相当于 private。这意味着它不能被其他包中的类直接访问,但可以被同一包中的任何类访问。这是比private更开放,但比protected和public更受限的访问级别。
String 是最基本的数据类型吗?
不是。Java 中的基本数据类型只有 8 个;除了基本类型 剩下的都是引用类型,Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
String 是不可变类。不可变类指的是无法修改对象的值,当你创建一个 String 对象之后,这个对象就无法被修改。像执行s += “a”; 返回的是一个新的 String 对象,老的 s 指向的对象不会发生变化,只是 s 的引用指向了新的对象而已。“不可变”最主要的好处就是安全,在多线程环境下也是线程安全的;然后,配合常量池可以节省内存空间,且获取效率也更高(如果常量池里面已经有这个字符串对象了,就不需要新建,直接返回即可)。String的初始值是?
NULL.String 常见题.
- “字面量创建字符串”:yesA 是一个引用指向了堆里面的字符串常量池里的对象 a。如果字符串常量池已经有了 abb,那么直接返回其引用,如果没有 abb,则会创建 abb 对象,然后返回其引用。
1
2
3String yesA = "abb";
String yesB = "abb";
System.out.printIn(yesA == yesB); // true - “new String创建字符串”:先判断字符串常量池里面是否有 abb,如果没有 abb 则创建一个 abb,然后会在堆内存里面创建一个对象 abb,返回堆内存对象 abb 的引用,也就是说返回的不是字符串常量池里面的 abb
1
2
3yesA = new String("abb");
yesB = new String("abb");
System.out.println(yesA == yesB); // false - intern():判断下 yesB 引用指向的值在字符串常量里面是否有,如果没有就在字符串常量池里面新建一个 aaabbb 对象,返回其引用,如果有则直接返回引用。
1
2
3
4
5String yesA = "aaabbb"; // 通过字面量定义了 yesA,在字符串常量池里创建 aaabbb 对象,返回其引用
String yesB = new String("aaa") + new String("bbb"); // 返回堆内的引用
String yesC = yesB.intern();
System.out.println(yesA == yesB); // false
System.out.println(yesA == yesC); // true - JDK 1.6 时,字符串常量池是放置在永久代的;
??? JDK 1.7 之后字符串常量池是放在堆内的1
2
3
4
5String yesB = new String("aaa") + new String("bbb"); // 此时,堆内会新建一个 aaabbb 对象,字符串常量池里不会创建,因为并没有出现 aaabbb 这个字面量。
String yesC = yesB.intern(); // 1.7 之后,如果堆内已经存在某个字符串对象的话,再调用 intern 此时不会在字符串常量池内新建对象,而是直接保存这个引用然后返回。
String yesA = "aaabbb"; // yesA 得到的引用与 yesC 和 yesB 一致,都指向堆内的 aaabbb 对象。
System.out.println(yesA == yesB); // true
System.out.println(yesA == yesC); // true
- “字面量创建字符串”:yesA 是一个引用指向了堆里面的字符串常量池里的对象 a。如果字符串常量池已经有了 abb,那么直接返回其引用,如果没有 abb,则会创建 abb 对象,然后返回其引用。
String 连接?
- 使用
+
运算符:字符串连接最简单的方法,但在大量连接操作时可能效率较低,因为它会生成多个临时的字符串对象。 - 使用
StringBuilder
:可变的字符序列,适用于需要频繁进行字符串连接的场景。append
方法用于添加字符串内容,最后使用toString
方法获取最终的字符串。适合单线程环境下使用。 - 使用
StringBuffer
:与StringBuilder
类似,也是可变的字符序列,但不同之处在于StringBuffer
是线程安全的,适用于多线程环境。
- 使用
String,StringBuffer 和 StringBuilder 的区别是什么?
String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象。
StringBuffer与StringBuilder都继承了AbstractStringBulder类,而AbtractStringBuilder又实现了CharSequence接口,两个类都是用来进行字符串操作的。在做字符串拼接修改删除替换时,效率比string更高。
StringBuffer是线程安全的,Stringbuilder是非线程安全的。所以Stringbuilder比stringbuffer效率更高,StringBuffer的方法大多都加了synchronized关键字String类的常用方法有那些?
charAt:返回指定索引处的字符
indexOf():返回指定字符的索引
replace():字符串替换
trim():去除字符串两端空白
split():分割字符串,返回一个分割后的字符串数组
getBytes():返回字符串的byte类型数组
length():返回字符串长度
toLowerCase():将字符串转成小写字母
toUpperCase():将字符串转成大写字符
substring():截取字符串
format():格式化字符串
equals():字符串比较分割字符串常用的方法。
split(String regex)
方法: 使用正则表达式来分割字符串。返回一个字符串数组,包含分割后的子字符串。1
2
3String input = "apple,orange,banana";
String[] fruits = input.split(",");
// fruits 数组: {"apple", "orange", "banana"}split
方法配合正则表达式的转义字符: 如果分隔符是正则表达式的元字符,需要进行转义。1
2
3String input = "apple.orange.banana";
String[] fruits = input.split("\\.");
// fruits 数组: {"apple", "orange", "banana"}substring
和indexOf
方法: 手动截取子字符串。1
2
3
4
5String input = "apple,orange,banana";
int commaIndex = input.indexOf(",");
String firstPart = input.substring(0, commaIndex);
String secondPart = input.substring(commaIndex + 1);
// firstPart: "apple", secondPart: "orange,banana"- 使用
Pattern
和Matcher
类: 进行更复杂的正则表达式匹配和分割。1
2
3
4
5
6
7import java.util.regex.Pattern;
import java.util.regex.Matcher;
String input = "apple,orange;banana";
Pattern pattern = Pattern.compile("[,;]");
String[] fruits = pattern.split(input);
// fruits 数组: {"apple", "orange", "banana"}
JDK9为什么要将 String 的底层实现由 char[] 改为 byte[]?
jdk中字符用utf-16编码(UTF-16是Unicode的一种实现方式,它使用16位的编码单元来表示一个字符),一个字符char要占用2个字节;但是对于由纯英文字符和ascii字符组成的字符串,只需要一个字节就可以表示所有ascii字符,使用 byte[] 可以节省一半空间。
只有在需要存储非ascii字符时,才会使用char[]Unicode 和 UTF-8 的区别:
- Unicode(统一码):
Unicode 是一种字符集(Character Set),用于定义字符和字符编码之间的对应关系。
Unicode 中包含了世界上几乎所有的字符,包括各种语言的文字、符号、表情符号等。
Unicode 采用 16 位(2 字节)来表示一个字符,因此可以表示的字符范围很广泛,共有 65536 个码位(Code Point)。 - UTF-8(Unicode Transformation Format-8):
UTF-8 是一种变长的字符编码方式,用于将 Unicode 字符编码成字节序列。
UTF-8 的最大特点是兼容 ASCII 码,即 ASCII 码中的字符(0-127)与 UTF-8 中的编码是相同的,这使得 UTF-8 在 Web 上具有广泛的应用。
UTF-8 使用 1 到 4 个字节来表示一个 Unicode 字符,根据字符的不同而变化,因此可以灵活地表示各种字符,包括中文、日文、韩文等等。
- Unicode(统一码):
++i与i++的区别
- i++:先赋值,后计算;
- ++i:先计算,后赋值
- 在JVM层面,这两个操作的实现是通过指令集中的不同指令来完成的。Java虚拟机中的字节码指令包含 iinc 指令用于递增局部变量的值。这两种递增操作在底层都是通过 iinc 指令实现的,但在具体的使用上有一些差异。
1
2
3
4getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i1
2
3
4getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
count += –count; 值为?
1、--count
表示先对count
执行减一操作,然后将结果赋给count
。
2、 然后将count
的当前值(执行了减一操作后的值)加上count
的当前值,再将结果赋给count
。a=a+b 与 a+=b 有什么区别吗?
- += 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,
而a=a+b则不会自动进行类型转换.如:1
2
3
4
5
6byte a = 127, b = 127;
b = a + b; // 报编译错误:cannot convert from int to byte
b += a;
short s1= 1;
s1 = s1 + 1; // 编译器会报错.short类型在进行运算时会自动提升为int类型,也就是说 s1+1 的运算结果是int类型,而s1是short类型,.
s1 += 1;
- += 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,
&和&&的区别
&是位运算符。&&是布尔逻辑运算符
在进行逻辑判断时用&处理的前面为false后面的内容仍需处理,用&&处理的前面为false不再处理后面的内容。200 + null 值为?
Java 中,如果对一个整数和 null 值进行加法运算,会导致编译错误。在运行时,如果存在 null 值参与运算,会抛出 NullPointerException 异常。(-10) % (-3) = ?
- 如果被取模数为正数,结果的符号与被取模数相同。10 = 3 * 3 + 1;
- 如果被取模数为负数,结果的符号与除数相同。 (-10) = (-3) * 3 + (-1); 所以
(-10) % (-3)
= -1。
Java 常用的类,包,接口。
类:BufferedReader BufferedWriter FileReader FileWirter String Integer
常用的包:java.lang java.awt java.io java.util java.sql Java.net Java.math
常用的接口:Remote List Map Document NodeListObject类常用方法有那些?
Equals
Hashcode
toString
wait
notify
clone
getClassequals与==的区别
- ==:比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。
1、比较的是操作符两端的操作数是否是同一个对象。
2、两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。
3、比较基本数据类型的==
操作符直接比较它们的值,值相等则为true。如:int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。而对于 Integer a =10 与 Long b = 10L, 使用==
比较的是对象的引用而不是值,结果为 false。 - equals:用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。
- ==:比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。
equals()
和hashCode()
方法。为什么重写equals()
时通常也需要重写hashCode()
?equals()
方法:用于比较两个对象是否在逻辑上相等。默认实现是比较对象的内存地址(相当于 ==),即两个对象是否是同一个对象。在实际应用中,一般需要根据对象的业务含义重写该方法,比较对象的实际内容。hashCode()
方法:用于获取对象的哈希码,返回一个整数。哈希码是一种用于快速查找的技术,通常在集合(如 HashMap、HashSet)中用到。它可以帮助确定对象在哈希表中的存储位置,提高查找的效率。默认实现是c++编写的native方法,基于对象的内存地址生成哈希码。在实际应用中,一般需要在类中重写该方法,以便相等的对象具有相同的哈希码。
但是,不是同一个对象,使用hashCode()返回的int值(取值范围2^32)也可能相等,即发生了hash冲突。- 关系:在使用哈希表的集合中,
hashCode()
和equals()
之间存在一定的关系。如果两个对象通过equals()
方法比较相等,它们的hashCode()
应该返回相同的值。这是为了保持一致性,使得相等的对象在哈希表中能够正确地识别和处理。确保相等的对象具有相同的哈希码,从而使得在集合中正确地处理相等性。
如果两个对象通过equals()
方法比较相等,但它们的hashCode()
不相等,那么当放入哈希表等集合中时,它们将被视为不同的对象。这可能导致哈希表中存在相等的对象,破坏了集合的一致性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class MyClass {
private int id;
private String name;
// Constructors, getters, setters...
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
MyClass myClass = (MyClass) obj;
return id == myClass.id && Objects.equals(name, myClass.name);
}
public int hashCode() {
return Objects.hash(id, name);
}
}
有没有可能两个不相等的对象有相同的hashcode?
有可能,即产生hash冲突。当hash冲突产生时,一般有以下几种方式来处理:- 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.
- 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
- 再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突.
java中有没有指针?java中是值传递引用传递?
有指针,但是隐藏了,开发人员无法直接操作指针,由jvm来操作指针
java都是值传递,对于基本数据类型,传递是值的副本,而不是值本身。对于对象类型,传递是对象的引用,当在一个方法操作操作参数的时候,其实操作的是引用所指向的对象。构造方法 constructor
- 构造方法是Java类中的一种特殊方法(可省略),用于在创建对象时进行初始化操作。
构造方法的名称必须与类名相同,并且没有返回类型,包括 void。构造方法通常用于设置对象的初始状态,为对象的属性赋初值或执行其他初始化任务。 - 方法也可以与class同名,区别在于方法必须要有void或具体返回值类型;
- 一个类可以拥有多个构造方法,只要它们的参数列表不同。这称为构造方法的重载。
1
2
3
4
5
6
7public class MyClass {
private int value;
// 无参构造方法
public MyClass() {this.value = 0;}
// 有参构造方法
public MyClass(int value) {this.value = value;}
} - 构造方法能不能显式调用?
不能,构造方法当成普通方法调用,只有在创建对象的时候它才会被系统调用 - 构造方法能不能重写,重载?
可以重载,但不能重写。
- 构造方法是Java类中的一种特殊方法(可省略),用于在创建对象时进行初始化操作。
Java中有各种不同的类和代码块类型,让我们逐个解释它们:
- 普通类:是最基本的类类型,用于创建对象。它可以包含字段、方法、构造方法等。
1
2
3public class MyClass {
// Fields, methods, constructors, etc.
} - 构造方法:是包含在类中的一组语句块,没有使用任何关键字。它在对象每一次创建时执行,可以用于初始化对象。
构造代码块是定义在类中的,不带任何修饰符(例如public、private等)。它在每次创建对象时都会执行,执行的时机在构造器调用之前。与实例初始化块不同,构造代码块不能被单独调用。1
2
3
4
5
6
7
8
9public class MyClass {
MyClass() {
// 构造器
}
{
// 普通代码块
// 在对象创建时执行
}
} - 内部类:内部类是定义在另一个类内部的类。它有访问外部类成员的权限,并且可以用于实现一些封装和逻辑组织。
1
2
3
4
5public class OuterClass {
class InnerClass {
// 内部类
}
} - 外部类: 外部类是普通的顶级类,不嵌套在其他类中。
1
2
3public class OuterClass {
// 外部类
} - 静态代码块: 使用
static
关键字,包含在类中,用于在类加载时执行初始化操作。它仅执行一次。1
2
3
4
5
6public class MyClass {
static {
// 静态代码块
// 在类加载时执行一次
}
} - 静态内部类:是定义在另一个类内部的类,使用
static
修饰。与非静态内部类不同,它不依赖于外部类的实例。1
2
3
4
5public class OuterClass {
static class StaticInnerClass {
// 静态内部类
}
}
- 普通类:是最基本的类类型,用于创建对象。它可以包含字段、方法、构造方法等。
静态 static
- 静态变量(Static Variables):被声明为
static
的成员变量,属于类而不是类的实例。它被所有类的实例共享,只有一个副本存在于内存中。1
2
3
4
5public class Counter {
// 私有静态变量属于类而不属于类的实例,并且只能在类的内部访问
private static int count = 0;
public static void increment() { count++; }
} - 静态方法(Static Methods):被声明为
static
的方法,它不需要实例化类就可以直接通过类名调用。静态方法不能访问非静态成员,也无法使用this
关键字。
静态方法凭什么不能访问成员方法:因为成员方法属于对象实例,静态方法属于类本身,静态方法第一次加载(方法区)的时候还没有对象(堆),也就无法调用成员方法 - 静态代码块(Static Blocks):是包含在类中的静态块,它在类加载时执行,并且只执行一次。通常用于初始化静态变量或执行一些静态的初始化操作。
- 静态内部类(Static Inner Classes):在类中使用
static
关键字修饰的内部类。静态内部类与外部类实例无关,可以直接通过外部类名访问。
在使用静态成员时需要注意,它们的生命周期与类的生命周期相同,当类加载时会被初始化。静态成员属于类而不是对象,在合适的场景下能提供便利和效率。然而,过度使用静态成员可能会导致耦合度高和难以测试等问题,因此需要根据实际情况慎重使用。
- 静态变量(Static Variables):被声明为
内部类与静态内部类的区别?
- 静态内部类相对与外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果要访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变量、调用静态的方法;
- 普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方法。
- 如果外部类要访问内部类的属性或者调用内部类的方法,必须要创建一个内部类的对象,使用该对象访问属性或者调用方法。
- 如果其他的类要访问普通内部类的属性或者调用普通内部类的方法,必须要在外部类中创建一个普通内部类的对象作为一个属性,外同类可以通过该属性调用普通内部类的方法或者访问普通内部类的属性。
- 如果其他的类要访问静态内部类的属性或者调用静态内部类的方法,直接创建一个静态内部类对象即可。
静态变量、静态代码块、普通代码块和构造方法的执行顺序?
执行顺序可以总结为:静态变量(按定义顺序初始化) -> 静态代码块(按定义顺序执行) -> 普通代码块(对象实例化时按照定义顺序执行) -> 构造方法。- 静态变量(静态成员变量):在类加载时按照定义的顺序依次执行初始化,不论该变量在类中定义的位置如何,只会初始化一次。
- 静态代码块(Static Blocks):静态代码块在类加载时执行,优先于普通代码块和构造方法。静态代码块只会执行一次。
- 构造代码块(普通初始化块):构造代码块在对象实例化时执行,在构造方法之前执行。每次创建对象都会执行一次。
- 构造方法(Constructor):构造方法在对象创建时执行,用于初始化对象。构造代码块执行完毕后执行。
子类继承父类,且都包含静态方法、构造方法,那么静态变量、静态代码块、普通代码块和构造方法的执行顺序?
顺序:父类静态方法 -> 父类静态代码块 -> 子类静态方法 -> 子类静态代码块 -> 父类构造代码块 -> 父类构造方法 -> 子类构造代码块 -> 子类构造方法final在java中的作用,有哪些用法?
- 被final修饰的类不可以被继承
- 被final修饰的方法不可以被重写
- 被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
- 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
- 被final修饰的常量,在编译阶段会存入常量池中.
除此之外,编译器对final域要遵守的两个重排序规则更好:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序初次读一个包含fifinal域的对象的引用,与随后初次读这个fifinal域,这两个操作之间不能重排序
Java中的继承是单继承还是多继承?
类是不支持多继承的,只能有一个父类,
类可以实现多个接口,这样就可以达到类对多个接口的”多继承”效果。接口(interface)是一种特殊的抽象类,它可以被类实现(implements)而不是被继承(extends)。接口中定义了一组抽象方法和常量,实现该接口的类必须实现接口中定义的所有抽象方法,并且可以拥有自己的字段和方法。super() 与 this() 表示什么?
- super表示当前类的父类对象,This表示当前类的对象。
super()
和this()
都是特殊的方法调用语句,用于调用构造器,并且都只能在构造器中使用,不能在普通方法中使用。另外,调用构造器时不能形成循环调用,即不能在同一个构造器中同时调用super()
和this()
。 super()
:用于调用父类的构造器。在子类的构造器中使用 super() 可以显式调用父类的构造器,并且必须作为子类构造器的第一条语句出现。如果子类构造器没有显式调用super()
,则会默认调用父类的无参构造器。1
2
3
4
5
6public class Child extends Parent {
public Child() {
super(); // 调用父类的无参构造器
System.out.println("Child constructor");
}
}this()
:用于调用当前类的其他构造器。在一个类的构造器中使用 this() 可以调用同一类中的其他构造器,并且必须作为构造器的第一条语句出现。1
2
3
4
5
6
7
8
9
10
11
12
13public class MyClass {
private int value;
public MyClass() {
this(0); // 调用当前类的带参构造器
System.out.println("Default constructor");
}
public MyClass(int value) {
this.value = value;
System.out.println("Parameterized constructor");
}
}
- super表示当前类的父类对象,This表示当前类的对象。
抽象类(Abstract Class):
- 特点:抽象类是一种不能被实例化的类,通常用于定义其他类的结构和行为。它可以包含抽象方法(只有方法签名,没有具体实现),以及普通的方法和字段。一个类只能继承一个抽象类。可以包含构造函数,可以有访问修饰符(public、private、protected)的方法。子类必须实现抽象类中的所有抽象方法,除非子类也是抽象类。
- 使用场景:当需要创建一个类,并在其中定义一些方法的行为,但不希望该类被实例化时,可以使用抽象类。抽象类也适合用于在类层次结构中作为其他类的基类,提供通用方法和字段,而具体实现交给其子类。
- 抽象方法的方法体不需要使用
{}
,抽象方法是指没有具体实现的方法。只需在方法签名后面加上分号即可,不需要提供方法体。1
2
3
4
5
6
7
8public abstract class AbstractClass {
// 抽象方法,没有方法体
public abstract void abstractMethod();
// 具体方法,有方法体
public void concreteMethod() {
System.out.println("This is a concrete method.");
}
}
普通类与抽象类有什么区别?
普通类不能包含抽象方法,抽象类可以包含抽象方法;
抽象类不能直接实例化,普通类可以直接实例化抽象的方法是否可同时是静态的,是否可同时是本地方法(native),是否可同时被 synchronized修饰?
都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。abstract 和 final 同时用来修饰同一个类或方法?
如果一个类使用了abstract
修饰符,表示这个类是抽象类,不能被实例化,可以包含抽象方法。
如果一个类使用了final
修饰符,表示这个类是最终类,不能被其他类继承。
如果一个方法使用了public abstract
修饰符,表示这个方法是抽象方法,只有声明没有实现,需要在子类中实现。
所以,abstract final
不能同时修饰类或方法。public abstract final
也是不符合Java语法的组合。需要根据具体的需求和设计来选择适当的修饰符。是否可以从一个静态方法内部发出对非静态方法的调用?
不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。接口(Interface):
- 特点:接口是一种完全抽象的类别,其中只包含方法的签名,但没有方法的实际实现。类可以实现多个接口,但接口不能包含字段或非抽象方法(在Java 8之后,引入了默认方法和静态方法)。接口就是某个事物对外提供的一些功能的声明,是一种特殊的java类,接口弥补了java单继承的缺点。
- 使用场景:当不同类需要共享某些行为,但它们属于不同的类层次结构时,接口是一个很好的选择。接口允许类定义一组规范,以确保实现类必须提供接口中定义的所有方法。可以使用接口来实现多态性,允许不同的类实现相同的接口并具有不同的行为。
- 接口有什么特点?
接口中声明全是public static final修饰的常量
接口中所有方法都是抽象方法
接口是没有构造方法的
接口也不能直接实例化
接口可以多继承
接口与抽象类!?
- 接口和抽象类都是为了实现代码的重用和提供一致的编程接口而设计的。然而,接口更多地用于定义规范和合同,以确保实现类提供特定的行为,而抽象类更多地用于提供一些通用的方法和行为实现。
- 抽象类和接口都用于实现多态性和提供一致的编程接口。它们通常用于大型项目中的类层次结构设计和代码组织。
- 在设计框架或库时,接口是一个有用的工具,因为它可以定义规范和标准,并允许用户通过实现接口来提供自定义行为。
- 抽象类用于将一些通用方法和字段提取到一个父类中,以便子类可以继承和共享这些功能。
- 抽象类和接口的区别?
- 抽象类:1. 抽象方法,只有行为的概念,没有具体的行为实现。使用abstract关键字修饰,没有方法体。子类必须重写这些抽象方法。2. 包含抽象方法的类,一定是抽象类。3. 抽象类只能被继承,一个类只能继承一个抽象类。
- 接口:1. 全部的方法都是抽象方法,属性都是常量 2. 不能实例化,可以定义变量。3. 接口变量可以引用具体实现类的实例 4. 接口只能被实现,一个具体类实现接口,必须实现全部的抽象方法 5. 接口之间可以多实现 6. 一个具体类可以实现多个接口,实现多继承现象
- 抽象类和接口有什么异同?
抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是 private、默认、protected、public 的,而接口中的成员全都是 public 的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
- 接口和抽象类都是为了实现代码的重用和提供一致的编程接口而设计的。然而,接口更多地用于定义规范和合同,以确保实现类提供特定的行为,而抽象类更多地用于提供一些通用的方法和行为实现。
接口是否可继承接口?抽象类是否可实现接口?抽象类是否可继承具体类?
接口可以继承接口 ,而且支持 多重继承 。 抽象类 可以 实现接口 , 抽象类 可继承 具体类 也可以 继承抽象类 。Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来实现事件监听和回调。内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。匿名类,匿名内部类。
Java中的两个相关但不同的概念,它们通常用于创建临时的、一次性的类实例。- 匿名类:
- 概念: 匿名类是指没有明确命名的类,通常用于创建一个实现某个接口或继承某个类的对象。
- 语法: 匿名类的语法形式是通过
new
关键字创建一个对象的同时实现接口或继承类,并在花括号内定义类的实现。1
2
3SomeInterface obj = new SomeInterface() {
// 匿名类的实现
}; - 使用场景: 匿名类通常用于创建简单的、一次性的类实例,不需要为其定义专门的类名。
- 匿名内部类:
- 概念: 匿名内部类是指定义在其他类内部、没有类名的类。通常使用它来实现接口或继承类,并在类的内部进行实现。
- 语法: 匿名内部类的语法形式与匿名类相似,但它通常在其他类的方法内部定义,而不是在类的成员变量或其他地方。
1
2
3
4
5
6
7public class SomeClass {
public void doSomething() {
SomeInterface obj = new SomeInterface() {
// 匿名内部类的实现
};
}
} - 使用场景: 匿名内部类通常用于在方法内部创建一个实现某个接口或继承某个类的临时对象,它有助于简化代码结构,避免为一次性的需求专门定义一个新的类。
总体而言,匿名类和匿名内部类都是用于创建临时的、一次性的类实例,通常在需要实现某个接口或继承某个类的情况下使用。在Java中,Lambda 表达式的引入也提供了一种更简洁的方式来实现函数接口的匿名类。
- 匿名类:
Java的四种引用,强弱软虚
- 强引用:是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收
1
String str = new String("str");
- 软引用:在程序内存不足时,会被回收,使用方式: 可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
1
2
3// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str")); - 弱引用:只要JVM垃圾回收器发现了它,就会将之回收,使用方式: 可用场景:Java源码中的j的java.util.WeakHashMap中的key就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
1
WeakReference<String> wrf= newWeakReference<String> (str);
- 虚引用:虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue,使用例子: 可用场景:对象销毁前的一些操作,比如说资源释放等。Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效
1
PhantomReference<String> prf = newPhantomReference<String>(newString("str"),newReferenceQueue<>());
- ???上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用
- 强引用:是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收
注解 Annotation
注解在我的理解下,就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理。注解本身只是元数据,它本身不执行任何功能。具体的实现通常是在某个拦截器、切面(Aspect)或其他处理机制中完成的。
注解在开发中是非常常见的,比如Spring框架的 @Controller / @Param / @Select 等等。一些项目也用到lombok的注解,@Slf4j / @Data 等等。Java原生也有@Overried、@Deprecated、@FunctionalInterface等基本注解。
Java原生的基本注解大多数用于「标记」和「检查」还,此外有一种叫做元Annotation(元注解),所谓的元Annotation就是用来修饰注解的。
那你自己写过注解吗?
@Passtoken,,测试时加上此注解,发送请求时不用验证登录信息,,
JunboRestResponse:使用该注解,确保被注解的方法或类返回一个特定的“骏伯响应”格式。???如何实现??4种标准元注解是哪四种?
元注解的作用是负责注解其他注解。Java5.0 定义了 4 个标准的 meta-annotation 类型,被用来提供对其它 annotation 类型作说明。- @Target 修饰的对象范围
@Target说明了Annotation所修饰的对象范围: Annotation可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target可更加明晰其修饰的目标 - @Retention 定义 被保留的时间长短
Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)
由:1. SOURCE:在源文件中有效(即源文件保留)2. CLASS:在 class 文件中有效(即 class 保留)3. RUNTIME:在运行时有效(即运行时保留)4. - @Documented 描述-javadoc
@Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。 - @Inherited 阐述了某个被标注的类型是被继承的
@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该class 的子类。
- @Target 修饰的对象范围
final、finalize()、finally
- 性质不同:1. final为关键字;2. finalize()为方法;3. finally为区块标志,用于try语句中;
- 作用:
- final为用于标识常量的关键字,final标识的关键字存储在常量池中(在这里final常量的具体用法将在下面进行介绍);
- finalize()方法在Object中进行了定义,用于在对象“消失”时,由JVM进行调用用于对对象进行垃圾回收,类似于C++中的析构函数;用户自定义时,用于释放对象占用的资源(比如进行I/0操作);
- finally{}用于标识代码块,与try{}进行配合,不论try中的代码执行完或没有执行完(这里指有异常),该代码块之中的程序必定会进行;且finally{}中的 return也会比 try{}中的更早返回!!
Java中的异常体系是怎样的?
Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。- Error表示非常严重的错误,比如 java.lang.StackOverFlowError 和 Java.lang.OutofMemoryError,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机、磁盘、操作系统层面出现的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不大,因为程序可能已经根本运行不了了。
- Exception表示异常,表示程序出现Exception时,是可以靠程序自己来解决的比如NullPointerException、legalAccessException等,我们可以捕获这些异常来做特殊处理。
Exception这种异常又分为两类:运行时异常 和 编译异常。- 运行时异常(不受检异常):RuntimeException类及其子类表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
- 编译异常(受检异常):Exception中除RuntimeException极其子类之外的异常。如果程序中出现此类异常,比如说IOException、FileNotFoundException、SQLException,必须对该异常进行处理,否则编译不通过。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。
如何自定义一个异常
继承一个异常类,通常是RumtimeException或者Exception异常的处理机制有几种?
异常捕捉:try…catch…finally,异常抛出:throws。try catch finally,try里有return,finally还执行么?
执行,并且finally的执行早于try里面的return。结论:
1、不管有木有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。throw与thorws区别
- 位置不同
- throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
- 功能不同:
- throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
- throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。
- 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
- 位置不同
在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?
- 异常相当于一种提示,如果我们抛出异常,就相当于告诉上层方法,我抛了一个异常,我处理不了这个异常,交给你来处理,而对于上层方法来说,它也需要决定自己能不能处理这个异常,是否也需要交给它的上层。
- 所以我们在写一个方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己来处理,那就捕获该异常并进行处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public class ExampleService {
public void lowerLevelOperation() throws SpecificException {
// 一些可能抛出 SpecificException 的操作
throw new SpecificException("Something went wrong at lower level");
}
public void higherLevelOperation() throws HigherLevelException {
try {
// 调用底层方法
lowerLevelOperation();
} catch (SpecificException e) {
// 捕获底层方法抛出的 SpecificException
// 记录异常信息或进行其他处理
System.out.println("Caught specific exception: " + e.getMessage());
// 封装并递交异常给更高层
throw new HigherLevelException("Exception at higher level", e);
}
}
public static void main(String[] args) {
ExampleService service = new ExampleService();
try {
// 调用更高层的方法
service.higherLevelOperation();
} catch (HigherLevelException e) {
// 捕获更高层方法抛出的 HigherLevelException
// 记录异常信息或进行其他处理
System.out.println("Caught higher level exception: " + e.getMessage());
}
}
}
Java 泛型、、
- 在Java中的泛型简单来说就是:在创建对象或调用方法的时候才明确下具体的类型
使用泛型的好处就是代码更加简洁(无需强制转换),程序更加健壮(编译期间没有警告,在运行期就无ClassCastException) - 使用场景:操作集合的时候,List
lists = new ArrayList<>();
如果是其他场景的话,那就是在写「基础组件」的时候了:再明确一下泛型就是「在创建对象或调用方法的时候才明确下具体的类型」,而组件为了做到足够的通用性,是不知道「用户」传入什么类型参数进来的,所以在这种情况下用泛型就是很好的实践。 - 泛型是会擦除的,那为什么反射能获取到泛型的信息呢?
泛型的信息只存在编译阶段,在class字节码就看不到泛型的信息了。那为什么下面这段代码能获取得到泛型的信息呢?
可以理解为泛型擦除是有范围的,定义在类上的泛型信息是不会被擦除的。
Java 编译器仍在 class 文件以 Signature 属性的方式保留了泛型信息。Type作为顶级接口,Type下还有几种类型,比如TypeVariable、ParameterizedType、WildCardType、GenericArrayType、以及Class。通过这些接口我们就可以在运行时获取泛型相关的信息。 - 泛型实例:组件为了做到足够的通用性,是不知道「用户」传入什么类型参数进来的,所以在这种情况下用泛型就是很好的实践。要写组件,还是离不开Java反射机制(能够从运行时获取信息),所以一般组件是泛型 + 反射来实现的。
1
2
3
4
5
6
7
8
9// 抽象类,定义泛型<T>
public abstract class BaseDao<T> {
public BaseDao(){
Class clazz = this.getClass();
ParameterizedType pt = (ParameterizedType) clazz.getGenericSuperclass();
clazz = (Class) pt.getActualTypeArguments()[0];
System.out.println(clazz);
}
}1
2
3
4
5
6// 实现类
public class UserDao extends BaseDao<User> {
public static void main(String[] args) {
BaseDao<User> userDao = new UserDao();
}
}1
2// 执行结果输出
class com.entity.User
- 在Java中的泛型简单来说就是:在创建对象或调用方法的时候才明确下具体的类型
泛型中extends和super的区别
1、<?extends T> 表示包括T在内的任何T的子类
2、<?super T> 表求包括T在内的任何T的父类Java对象创建过程?new一个对象的步骤?
- ???检查类符号引用:首先,JVM 会检查 new 关键字后面的类符号引用,确保在常量池中能找到对应的类。???2. 加载类:如果在常量池中找到了类符号引用,JVM 就会加载这个类,包括加载、连接(验证、准备、解析)和初始化阶段。
- (1. )类加载检查:JVM 首先检查是否已经加载并验证了这个类的字节码。如果没有,JVM会通过类加载器(ClassLoader)加载这个类,并进行验证,确保其符合Java语言规范。这个过程可能包括解析类的依赖关系,解析字段和方法,以及进行类型检查。
- 分配内存:JVM 会根据类的定义,在堆内存中为对象分配内存空间。这个内存空间包括对象的所有成员变量。这个过程包括选择内存分配方式(如指针碰撞、空闲列表、TLAB)、分配内存并进行内存清零。
- 初始化对象:分配完内存空间后,JVM 会将对象的除对象头外的内存空间初始化为默认值(基本数据类型为 0,引用类型为 null)。最后,JVM 会设置对象的对象头,包括哈希码、GC 信息等元信息。
- 调用构造方法:执行对象的初始化逻辑,包括对成员变量进行赋值、执行一些初始化操作等。
- 返回对象引用:最后,new 操作符会返回一个指向新创建对象的引用,通过这个引用可以在程序中操作对象的属性和调用对象的方法。
- 这些步骤是创建一个对象的基本流程,无论是通过 new 关键字创建对象,还是通过反射、序列化等方式创建对象,都要经历这些步骤。
Java 反射?
简单来说,反射就是Java可以给我们在运行时获取类的信息
什么是「运行时」:在编译器写的代码是 .java 文件,经过javac 编译会变成 .class 文件,class 文件会被JVM装载运行(这里就是真正运行着我们所写的代码(虽然是被编译过的),也就所谓的运行时。
为什么要在「运行时」获取类的信息:其实就是为了让我们所写的代码更具有「通用性」和「灵活性」。一个好用的“工具”是需要兼容各种情况的,不知道用该“工具”的用户传入的是什么对象,但你需要帮他们得到需要的结果。例如 SpringMVC 你在方法上写上对象,传入的参数就会帮你封装到对象上;Mybatis可以让我们只写接口,不写实现类,就可以执行SQL;在类上加上@Component注解,Spring就帮你创建对象…
这些统统都有反射的身影:约定大于配置,配置大于硬编码。通过”约定”使用姿势,使用反射在运行时获取相应的信息(毕竟作为一个”工具“是真的不知道你是怎么用的),实现代码功能的「通用性」和「灵活性」除了使用new创建对象之外,还可以使用 Java 反射可以创建对象,谁的效率高?
通过new创建对象的效率比较高。通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低。用 new 关键字创建对象到底是编译时的还是运行时的方式?有什么区别?
new 创建对象是一种在编译时进行的方式。在编写代码时,通过 new 关键字可以直接在源代码中创建对象,在源代码被编译成字节码时就确定了对象的创建,然后在运行时,Java 虚拟机(JVM)会加载字节码文件,并根据 new 关键字创建对象。这时会分配内存、调用构造方法等,完成对象的初始化。
这种方式的主要特点是静态,因为对象的创建和初始化都是在编译时确定的。相比之下,使用反射等机制可以实现在运行时动态创建对象,但也更为灵活,因为它可以处理一些在编译时无法确定的类型和类。java反射的作用
反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。哪里会用到反射机制?
- jdbc就是典型的反射,hibernate,struts等框架使用反射实现的。
1
Class.forName('com.mysql.jdbc.Driver.class'); // 加载MySQL的驱动类
- 在 Spring 中使用反射机制:目的是为了实现框架的灵活性和可扩展性,使得开发人员能够通过配置和注解等方式,实现各种功能而无需修改源代码:
- 依赖注入:Spring 使用反射来实现依赖注入,即通过在配置文件或注解中声明依赖关系,Spring 在运行时动态地注入对象之间的依赖关系。通过反射,Spring 能够实例化和初始化对象,以及在运行时处理依赖注入。
- Bean 的自动装配: Spring 的自动装配机制依赖于反射,它能够根据一定的规则自动将 Bean 与其他 Bean 进行关联。通过反射,Spring 可以动态地识别和连接相应的 Bean。
- AOP(面向切面编程): 在 Spring 中,AOP 是通过动态代理和反射来实现的。通过反射,Spring 能够在运行时动态地创建代理对象,并在方法执行前后执行额外的逻辑。
- Bean 的生命周期管理: Spring 容器可以通过反射来实现对 Bean 的生命周期的管理,包括实例化、初始化、销毁等。
- 动态代理:Spring 使用动态代理和反射来实现一些特定的功能,如事务管理。通过动态代理,Spring 能够在运行时创建代理对象,将横切逻辑织入到目标对象中。
- 处理注解:Spring 使用反射来处理注解,包括扫描类路径上的注解、解析注解的属性值等。通过反射,Spring 能够在运行时获取和处理注解信息。
- jdbc就是典型的反射,hibernate,struts等框架使用反射实现的。
反射机制的优缺点
- 优点:
- 能够运行时动态获取类的实例,提高灵活性;
- 与动态编译结合
- 缺点:
- 使用反射性能较低,需要解析字节码,将内存中的对象进行解析。解决方案:1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度;2、多次创建一个类的实例时,有缓存会快很多; 3、ReflflectASM工具类,通过字节码生成的方式加快反射速度
- 相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
- 优点:
反射的实现方式:
- 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法。以下是常用的方法:
- 使用
Class.forName
静态方法(最安全/性能最好)1
2
3
4
5try {
Class<?> myClass = Class.forName("com.example.MyClass"); // 类的全限定名作为参数
} catch (ClassNotFoundException e) {
e.printStackTrace();
} - 使用
ClassLoader
的loadClass
方法:也可以加载类并返回Class
对象。1
2
3
4
5
6ClassLoader classLoader = getClass().getClassLoader();
try {
Class<?> myClass = classLoader.loadClass("com.example.MyClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} - 使用
.class
语法: 在已知类的情况下,可以直接获取该类的Class
对象。1
Class<?> myClass = MyClass.class;
- 使用对象的
getClass
方法: 可以返回该对象的Class
对象。1
2MyClass myObject = new MyClass();
Class<?> myClass = myObject.getClass();
- 使用
- 调用 Class 类中的方法,获取类的结构信息。
Java 反射 API:用来生成 ??? JVM 中的类、接口或则对象的信息。- Class 类:反射的核心类,可以获取类的属性,方法等信息。
- Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
- Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
- Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。
1
2
3
4
5
6
7
8
9//获取 Person 类的 Class 对象
Class clazz = Class.forName("reflection.Person");
//获取 Person 类的所有方法信息
Method[] method = clazz.getDeclaredMethods();
//获取 Person 类的所有成员属性信息
Field[] field = clazz.getDeclaredFields();
//获取 Person 类的所有构造方法信息
Constructor[] constructor = clazz.getDeclaredConstructors();
for(Constructor c : constructor) System.out.println(c.toString());
- 将获取到的类信息用于实际的操作,例如创建对象、调用方法、获取和设置字段的值等。
通过反射 API 来实现动态操作。
- 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法。以下是常用的方法:
利用反射动态创建对象实例.
1
2
3
4
5
6
7
8// 0. 获取 Person 类的 Class 对象
Class clazz = Class.forName("reflection.Person");
// 1. 使用 newInstane 方法创建实例对象(这种方法要求该 Class 对象对应的类有默认的空构造器)
Person p = (Person) clazz.newInstance();
// 2. 先获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance()方法来创建 Class 对象实例
// (通过这种方法可以选定构造方法创建实例)
Constructor c = clazz.getDeclaredConstructor(String.class,String.class,int.class); // 获取构造方法
Person p1 = (Person) c.newInstance("李四","男",20); // 创建对象并设置属性深拷贝和浅拷贝
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
1,浅拷贝是指,只会拷贝基本教据类型的值,以及实例对象的引用地址,并不会复制一份引用地处所指的对象。也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
2,深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象你了解动态代理吗???
- 代理模式是设计模式之一。代理模型有静态代理和动态代理。
- 静态代理需要自己写代理类,实现对应的接口,比较麻烦,在【编译期间】就确定好代理关系。
- 动态代理这一技术在实际或者框架原理中是非常常见的,在【运行期间】确定好代理关系。
- 动态代理有什么用:它是一种设计模式,用于在不修改原始对象的情况下,通过代理对象来间接访问原始对象,并在访问前后执行额外的操作。
- 动态代理中的对象:
- 目标对象:待增强的对象
- 代理对象:增强过后的对象,也就是我们使用的对象
- 两种实现方式:
- JDK动态代理(反射)其实就是运用了反射的机制,会帮我们实现接口的方法,通过invokeHandler对所需要的方法进行增强;
- 目标对象 implement 目标接口
- 代理对象 implement 目标接口
- 目标对象和代理对象是平级关系
- CGLIB代理(继承)则用的是利用ASM框架,通过修改其字节码生成子类来处理。
- 代理对象 extend 目标对象
- 目标对象和代理对象是父子关系
- JDK动态代理(反射)其实就是运用了反射的机制,会帮我们实现接口的方法,通过invokeHandler对所需要的方法进行增强;
- 动态代理的应用:功能增强、控制访问。
- SpringAOP
- Spring整合Mybaits管理Mapper接口,不用写实现类,只写接口就可以执行SQL
- 代理模式是设计模式之一。代理模型有静态代理和动态代理。
面向对象设计中五大设计原则 SOLID、、
- 单一职责原则(SRP): 每个类或模块应该有且仅有一个引起它变化的原因,即一个类或模块应该只负责一种类型的任务或功能。这样可以提高代码的内聚性和可维护性,减少代码的复杂度。
- 开放封闭原则(OCP): 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即通过扩展现有的代码来实现新的功能,而不是修改已有的代码。这样可以降低修改已有代码带来的风险,并提高系统的稳定性。
- 里氏替换原则(LSP):子类必须能替换父类并出现在父类能够出现的任何地方,而不影响程序的正确性。即子类应该完全实现父类的方法,并且遵循父类的约定和契约。
- 接口隔离原则(ISP): 不应该强迫客户端依赖它们不需要的接口。接口应该小而专一,应该根据实际需要定义多个接口,而不是一个臃肿的接口。这样可以降低类之间的耦合度,提高系统的灵活性和可维护性。
- 依赖倒置原则(DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。这样可以降低模块之间的耦合度,提高系统的灵活性和可扩展性。
常用的设计模式、、
- 策略模式:定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的的客户。
- 使用场景:如果代码有多个 if…else 等条件分支,并且每个条件分支,可以封装起来替换的,我们就可以使用策略模式来优化。
- 模式实现:一个接口或者抽象类,里面两个方法(一个方法匹配类型,一个可替换的逻辑实现方法)、不同策略的差异化实现(即,不同策略的实现类)、使用策略模式
- 模式使用:我们借助 spring 的生命周期,使用 ApplicationcontextAware 接口,把对用的策略,初始化到 map 里面。然后对外提供 resolveFile 方法即可。
- 责任链模式:实际上是一种处理请求的模式,它让多个处理器(对象节点)都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。
- 使用场景:当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式。责任链上,每个对象的差异化处理,如本小节的业务场景,就有参数校验对象、安全校验对象、黑名单校验对象、规则拦截对象
- 模式实现:一个接口或者抽象类、每个对象差异化处理、对象链(数组)初始化(连起来)
- 这个接口或者抽象类,需要:有一个指向责任下一个对象的属性、一个没置下一个对象的set方法、给子类对象有异化实现的方法(如以下代码的doFiter方法)
- 模版方法模式:定义一个操作中的算法的骨架流程,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。它的核心思想就是:定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现,这样不同的子类就可以定义出不同的步骤。
- 模式实现:一个抽象类,定义骨架流程(抽象方法放一起)确定的共同方法步骤,放到抽象类(去除抽象方法标记)。不确定的步骤,给子类去差异化实现
- 观察者模式:行为模式,一个对象(被观察者)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
- 它的主要成员就是观察者和被观察者。
- 被观察者(0bserverable):目标对象,状态发生变化时,将通知所有的观察者。
- 观察者(observer):接受被观察者的状态变化通知,执行预先定义的业务。
- 使用场景:完成某件事情后,异步通知场景。如,登陆成功,发个M消息等等。
- 模式实现:一个被观察者的类Observerable、多个观察者Observer、观察者的差异化实现、经典观察者模式封装:EventBus实战
- 它的主要成员就是观察者和被观察者。
- 工厂模式:
- 工厂模式一般配合策略模式一起使用。用来去优化大量的if..else..或switc…cas…条件语句。
- 定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
- 单例模式:确保一个类只有一个实例,并提供一个全局访问点
- 懒汉模式、饿汉模式
- 策略模式:定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的的客户。
工厂模式和抽象工厂的区别
- 简单工厂模式:
- 简单工厂模式是一种创建型设计模式,它通过一个工厂类来封装对象的创建过程,用户只需要通过工厂类来获取所需的对象,而无需直接调用对象的构造方法。
- 简单工厂模式只包含一个工厂类和多个产品类,工厂类根据用户的请求返回不同的产品对象。
- 简单工厂模式适用于对象类型较少、不需要频繁变化的情况下,对于新增产品类型或者修改产品构造方法时,需要修改工厂类的代码。
- 抽象工厂模式:
- 抽象工厂模式也是一种创建型设计模式,它通过一个抽象工厂接口和多个具体工厂类来创建一组相关或者相互依赖的对象,而无需指定具体的类。
- 抽象工厂模式包含抽象工厂接口、具体工厂类、抽象产品接口和具体产品类,每个具体工厂类负责创建一组相关的产品对象。
- 抽象工厂模式适用于需要创建一组相关或者相互依赖的产品对象,并且对产品的具体类型和实现进行解耦的情况下,当需要新增产品类型时,只需要添加新的具体工厂类和对应的产品类,无需修改现有代码。
- 综上所述,简单工厂模式主要用于创建单一类型的对象,工厂类负责根据用户的请求返回相应的产品对象;而抽象工厂模式主要用于创建一组相关或者相互依赖的产品对象,通过抽象工厂接口和具体工厂类来实现对象的创建,并且对产品的具体类型和实现进行解耦,适用于产品类型频繁变化的情况。
- 简单工厂模式:
手写单例模式
- 懒汉式单例模式:只有在需要时才会创建实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class Singleton {
private static Singleton instance; // 私有静态变量,用于保存唯一实例
private Singleton() { } // 私有构造函数,防止外部实例化
public static Singleton getInstance() { // 公共静态方法,用于获取实例
if (instance == null) { // 使用双重检查锁定(double-checked locking)来确保线程安全
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
} - 饿汉式单例模式:如果需要在类加载时就创建实例,可以直接在静态变量中初始化
1
2
3
4
5
6
7
8
9public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
// 可以在这里进行一些初始化操作
}
public static Singleton getInstance() {
return instance;
}
}
- 懒汉式单例模式:只有在需要时才会创建实例
对序列化和反序列化的理解?
- 序列化:把内存中的对象转换为字节流,以便实现存储和运输
- 反序列化:根据从网络或文件获取的对象的字节流,根据字节流中保存的对象描述信息和状态,重新构建一个新的对象
- 序列化的目的是为了解决网络通信中的对象传输的问题,把当前jvm进程中的对象跨网络传输到另一个jvm进程中并恢复;为保证通信双方对对象的可识别,会把对象先转换为通用的解析格式如:json,xml、、再转换为字节流进行运输
- 实现方法:序列化对象实现
Serializable
接口,并再对象中添加serialVersionUID
字段。
什么时候用assert
assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。在实现中,assertion就是在程序中的一条语句,它对一个boolean表达式进行检查,一个正确程序必须保证这个boolean表达式的值为true;如果该值为false,说明程序已经处于不正确的状态下,系统将给出警告或退出。一般来说,assertion用于保证程序最基本、关键的正确性。assertion检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion检查通常是关闭的Java有没有goto
java中的保留字,现在没有在java中使用???拦截器与过滤器?
- 过滤器(Filter):
- 过滤器是 Servlet 规范中定义的一种组件,用于对请求进行预处理、后处理以及过滤。过滤器可以在请求进入 Servlet 之前进行预处理,也可以在响应返回客户端之前进行后处理。过滤器主要用于对请求和响应进行修改、验证、记录日志等操作。
- 过滤器可以通过在
web.xml
配置文件中进行配置,也可以通过注解@WebFilter
来声明。过滤器需要实现javax.servlet.Filter
接口,并实现其中的init
、doFilter
和destroy
方法。 - 过滤器可以对所有的请求进行过滤,例如对 URL 模式进行匹配,也可以通过编程方式动态地添加或移除过滤器。
- 拦截器(Interceptor):
- 拦截器是 Spring 框架提供的一种机制,用于对请求进行预处理和后处理。拦截器是基于面向切面编程(AOP)的思想,可以对控制器方法进行拦截,对请求进行前置处理、后置处理、异常处理等操作。
- 拦截器是 Spring MVC 框架的一部分,通过实现
HandlerInterceptor
接口来定义拦截器,并通过配置文件或者 Java 配置类进行声明和注册。 - 拦截器可以精确地对指定的控制器方法进行拦截,可以在请求处理之前或之后进行操作,并且可以对 Model 和 View 进行修改或者增强。
- 区别,主要体现在以下几个方面:
- 所处框架:
过滤器(Filter)是 Servlet 规范中的一部分,用于对请求和响应进行预处理和后处理。它是在 Web 容器层面的一种功能。
拦截器(Interceptor)是 Spring MVC 框架中的一部分,用于对控制器方法进行拦截和处理。它是在 Spring MVC 框架的控制器层面的一种功能。 - 实现方式:
过滤器需要实现javax.servlet.Filter
接口,并实现其中的init
、doFilter
和destroy
方法。过滤器可以通过web.xml
配置文件中进行配置,也可以通过注解@WebFilter
来声明。
拦截器需要实现HandlerInterceptor
接口,并实现其中的preHandle
、postHandle
和afterCompletion
方法。拦截器的声明和注册通常是通过配置文件或者 Java 配置类来完成。 - 功能特性:
过滤器对请求和响应进行处理,可以进行内容修改、请求重定向、日志记录等操作。过滤器可以对所有的请求进行过滤。
拦截器主要用于对控制器方法进行拦截,可以在请求处理之前或之后进行操作,例如权限验证、日志记录、异常处理等。拦截器可以精确地对指定的控制器方法进行拦截。 - 使用场景:
过滤器适用于对 Web 应用的全局请求进行处理,例如字符编码过滤、安全过滤、日志记录等。
拦截器适用于对控制器方法的请求进行处理,例如权限控制、日志记录、异常处理等。
- 所处框架:
- 过滤器(Filter):
日志、、
1.选择恰当的日志级别 error warn info debug
2.日志要打印出参入参数 方便甩锅
3.选择合适的日志格式 时间戳 线程名字 日志级别等
4.if-else ,switch 等分支语句都建议打印日志,方便排查
5.对一些比较低的日志级别进行判断,使用log.isXXXX()方法判断
6.不建议直接使用log4j ,logback等日志系统,建议使用slf4j框架,方便统一处理
7.建议使用参数占位符{},而不是+拼接,简洁且提升性能
8.建议使用异步日志,能有效提升IO性能
9.不要使用 e.printStackTrace() 打印错误信息,因为太多信息,且是堆栈信息,会使得内存溢出
10.异常不要只打一半,要完成输出
11.禁止在线上开启debug 会把磁盘打满
12.不要记录了异常,又抛出异常
13.避免重复打印日志,浪费磁盘空间
14.日志文件分离,不同级别日志存放在不同文件中
15.核心功能模块,建议打印详细的日志函数式接口、、
- 函数式接口具有以下主要特点:
函数式接口只包含一个抽象方法,但可以包含多个默认方法或静态方法。
函数式接口可以使用 @FunctionalInterface 注解来显式声明,这样可以让编译器进行检查,确保其满足函数式接口的定义。
函数式接口可以通过 Lambda 表达式、方法引用等方式进行实例化。 - 函数式接口的引入使得 Java 可以更加方便地支持函数式编程风格,包括:
更简洁的代码:通过 Lambda 表达式可以编写更加简洁、可读性更强的代码。
支持并行操作:函数式接口可以很好地配合 Stream API 使用,支持并行操作和函数式变成。
提升代码灵活性:函数式接口的使用可以提升代码的灵活性和可维护性,使得代码更易于扩展和修改。 - 函数式接口在实际应用中有很多场景,例如:
在并发编程中,可以使用函数式接口配合 CompletableFuture 来进行异步任务处理。
在集合操作中,使用函数式接口可以简化集合的筛选、映射等操作。
在事件处理和回调机制中,使用函数式接口可以定义事件处理器。
- 函数式接口具有以下主要特点:
Java 集合
集合有什么。
Java 集合 主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。Collection包结构,与Collections的区别
Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、Set;
Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的Collection框架。说说 List, Set, Map三者的区别
List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
Set(注重独一无二的性质):不允许重复的集合。不会有多个元素引用相同的对象。
Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,Key可以是任何对象。Java 的 List?
List在Java里边是一个接口,常见的实现类有ArrayList和LinkedList,ArrayList底层数据结构是数组,LinkedList链表。ArrayList
实现了动态扩容。当new ArrayList()时,默认会有一个大小为0,空的 Object[] 数组。
第一次add添加数据的时候,会给数组初始化一个默认值 10 的大小,并将元素添加到数组中。使用ArrayList在每一次add的时候,会先去计算数组空间;如果空间是够的,直接追加上去;如果不够,那就得扩容。
每一次扩容原来的 1.5倍,新容量的计算方式为(oldCapacity * 3)/2 + 1 即原容量的1.5倍再加1,创建一个新的数组[newData]。然后使用System.arraycopy方法(底层为native方法实现)将旧数组[elementData]中的元素复制到新数组中。这是一个高效的底层数组拷贝操作,避免了逐个元素复制的开销。最后,将新数组[newData]替换为ArrayList的内部数组[elementData]。
日常开发中用得最多的是ArrayList呢:是由底层的数据结构来决定的,在日常开发中,遍历的需求比增删要多,即便是增删也是往往在List的尾部添加就OK了。像在尾部添加元素,ArrayList的时间复杂度也就O(1)。LinkedList
基于链表实现的,对于增删操作来说,由于链表节点的指针调整相对比较简单,删除或添加一个节点的开销是 O(1) 的。但在进行遍历和随机访问时,由于链表的非连续存储,性能相对较差。因此,在实际场景中,ArrayList 在随机访问和遍历方面的性能通常比 LinkedList 更好,而在频繁的增删操作时,LinkedList 可能更具优势.
LinkedList 还实现了 DeQueue,可以对头尾元素操作,所以 LinkedList 也可以当作队列使用。Array与ArrayList有什么不一样?
Array与ArrayList都是用来存储数据的集合。ArrayList底层是使用数组实现的,但是ArrayList对数组进行了封装和功能扩展,拥有许多原生数组没有的一些功能。我们可以理解成ArrayList是Array的一个升级版。ArrayList 的
遍历
;- 使用 for 循环:
1
2
3for (int j = 0; j < list.size(); j++) {
System.out.println(list.get(j));
} - 使用增强型 for 循环(for-each 循环):
1
2
3for (Integer num : list) {
System.out.println(num);
} - 使用迭代器(Iterator):
1
2
3
4
5Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer num = iterator.next();
System.out.println(num);
}
- 使用 for 循环:
ArrayList 的
remove
、、- 根据索引删除元素;删除元素后,后面的元素会向前移动,列表的大小会减少。
1
list.remove(0); // 删除索引为 0 的元素
- 根据对象删除元素:remove(Object o) 方法可以删除列表中第一次出现的指定对象。
1
list.remove(Integer.valueOf(5)); // 删除值为 5 的元素
- 使用迭代器删除元素:使用 Iterator 遍历 ArrayList,并使用迭代器的 remove 方法安全地删除元素。
1
2
3
4
5
6
7Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer num = iterator.next();
if (num == 5) {
iterator.remove(); // 删除符合条件的元素
}
} - **
ConcurrentModificationException
**异常:在遍历过程中直接调用 remove() 可能会引起并发修改异常。这是因为在遍历过程中修改了集合结构,导致迭代器的检测机制检测到并抛出异常。为避免,可采用以下方法:- 在迭代过程中不修改集合的结构,可以通过复制集合或者使用迭代器的 remove 方法进行安全的删除操作。
- 在多线程环境下,可以使用线程安全的集合类(如 ConcurrentHashMap)或者采用同步机制(如使用 synchronized 关键字或者使用 Lock)来保证集合的线程安全性。
- 根据索引删除元素;删除元素后,后面的元素会向前移动,列表的大小会减少。
ArrayList如何实现线程安全?
- 使用
Collections.synchronizedList
方法:这将返回一个线程安全的 List 包装器。通过这种方式,对synchronizedList
的所有操作都会在内部被同步,从而确保线程安全。如果写入操作较为频繁,可能需要权衡使用 synchronizedList 或者其他并发集合类,具体根据业务需求来决定。1
List<Type> synchronizedList = Collections.synchronizedList(new ArrayList<Type>());
- 使用
CopyOnWriteArrayList
类:java.util.concurrent
包下的类,它通过在修改操作时复制整个数组来实现线程安全。这意味着在写入操作时,它会创建一个新的数组,从而不影响正在进行的读取操作。如果读取操作频繁而写入操作较少,CopyOnWriteArrayList
可能是更好的选择,因为它对于并发读取操作而言性能较好。1
List<Type> threadSafeList = new CopyOnWriteArrayList<Type>();
- 使用
CopyOnWriteArrayList的底层原理是怎样的
- 首先CopyOnWriteArraylst内部也是用过数组来实现的,在向CpyOnWriteAraist添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上行
- 并且,写操作会加锁,防止出现并发写入丢失数据的问题
- 写操作结束之后会把原数组指向新数组
- CopyOnWriteArraylist允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场,但会占内存,同时可能读到的数据不是实时最新的教据(写线程操作结束后才能读到新数据),所以不适合实时性要求很高的场景
Vector 你了解吗?
Vector是底层结构是数组,一般现在已经很少用了。相对于ArrayList,它是线程安全的,在扩容的时候直接扩容两倍。
在Java中,Stack 类扩展了Vector,提供了一个后进先出(LIFO)的堆栈数据结构,其中元素的插入和删除都发生在堆栈的顶部。set 集合
Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素, 值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号) 判断的, 如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。HashSet(Hash 表)
哈希表边存放的是哈希值。 HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。
元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true , HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。HashSet、HashMap 和 Hashtable 的关系。
HashSet
使用HashMap
作为底层实现,用于存储不重复的元素。HashMap
是键值对的存储结构,而HashSet
只存储键。Hashtable
也是键值对的存储结构,类似于HashMap
,但是是同步的,因此适合于多线程环境。HashMap
实现了Map
接口。Hashtable
实现了Map
接口以及Dictionary
接口(在 Java 1.0 和 1.1 版本中使用较多,现已被Map
接口取代)。HashMap
和Hashtable
允许键和值为null
,而HashSet
只允许一个null
元素。
在实际开发中,一般推荐使用HashMap
而不是Hashtable
,因为Hashtable
的同步性会带来额外的性能开销。如果需要在多线程环境下使用,也可以考虑使用Collections.synchronizedMap()
方法来创建一个同步的HashMap
。
TreeSet
- TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
- Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的, 自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareToTo()函数,才可以正常使用。
- 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序
- 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数
LinkHashSet( HashSet+LinkedHashMap)
对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。Map 了解多少?
Map在Java里边是一个接口,常见的实现类有 HashMap、LinkedHashMap、TreeMap 和 ConcurrentHashMap
在Java里边,哈希表的结构是数组+链表的方式。HashMap底层数据结构是数组+链表/红黑树;LinkedHashMap是数组+链表/红黑树+双向链表;TreeMap是红黑树;而ConcurrentHashMap是数组+链表/红黑树HashMap
- hashmap通过put(key,value)存储,通过get(key)获取。当传入key时,hashmap会调用hashcode()方法计算出hash值,根据 hash 值将 value 保存在 bucket 里
- 实现原理:其实就是有个 Entry 数组,Entry 保存了 key 和 value。当你要塞入一个键值对的时候,会根据一个 hash 算法计算 key 的 hash 值,然后通过数组大小
n-1 & hash
值之后,得到一个数组的下标,然后往那个位置塞入这个 Entry。(hashmap的底层是哈希表,哈希表的实现是数组+链表+红黑树) - 为了解决 hash 冲突的问题,采用了链表法
- 在 JDK 1.7 及之前链表的插入采用的是头插法,即在链表的头部插入新的 Entry。在 JDK 1.8 的时候,改成了尾插法,并且引入了红黑树。当链表的长度大于 8 且数组大小大于等于 64 的时候,就把链表转化成红黑树,当红黑树节点小于 6 的时候,又会退化成链表。
- 为什么 JDK 1.8 要对 HashMap 做红黑树这个改动?主要是避免 hash 冲突导致链表的长度过长,这样 get 的时候时间复杂度严格来说就不是 O(1) 了,因为可能需要遍历链表来查找命中的 Entry。
- 为什么定义链表长度为 8 且数组大小大于等于 64 才转红黑树?不要链表直接用红黑树不就得了吗?因为红黑树节点的大小是普通节点大小的两倍,所以为了节省内存空间不会直接只用红黑树,只有当节点到达一定数量才会转成红黑树,这里定义的是 8(泊松分布)
- 为什么节点少于 6 要从红黑树转成链表?也是为了平衡时间和空间,节点太少链表遍历也很快,节约内存。
- HashMap 默认大小为16,负载因子的大小为 0.75。
- HashMap的大小只能是2次幂的,假设你传一个10进去,实际上最终HashMap的大小是16(具体的实现在tableSizeFor可以看到)把元素放进HashMap的时候,需要算出这个元素所在的位置(hash)。在HashMap里用的是位运算来代替取模,更加高效。HashMap的大小只能是2次幂时,才能合理用位运算替代取模。
- 负载因子的大小决定着哈希表的扩容和哈希冲突。比如默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放16*0.75=12个元素,每次put元素进去的时候,都会检查HashMap的大小有没有超过这个阈值,一旦超过12,则哈希表需要扩容。如果把负载因子调高了,哈希冲突的概率会增高,同样会耗时(查找速度变慢了)
- put:首先对key做hash运算,计算出该key所在的index。如果没碰撞,直接放到数组中,如果碰撞了,需要判断目前数据结构是链表还是红黑树,根据不同的情况来进行插入。假设key是相同的,则替换到原来的值。最后判断哈希表如果满了,扩容。
- get:还是对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突。假设没有冲突直接返回,假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。
在HashMap中怎么判断一个元素是否相同?首先会比较hash值,随后会用==运算符和equals()来判断该元素是否相同。如果只有hash值相同,那说明该元素哈希冲突了,如果hash值和equals() || == 都相同,那说明该元素是同一个。 - Jdk1.7 到 Jdk1.8 HashMap 发生了什么变化(底层)?
- 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
- 1.7中链表插入使用的是头插法,1.8使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
- 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源
hash 函数的优化:
- 1.8后,在put元素的时候传递的Key,先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。相当于把高位和低位的特征进行组合,结果得到的数组位置的散列度一定会更高,可以增加了随机性,减少了碰撞冲突的可能性。
1
2
3
4static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 1.8后,在put元素的时候传递的Key,先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。相当于把高位和低位的特征进行组合,结果得到的数组位置的散列度一定会更高,可以增加了随机性,减少了碰撞冲突的可能性。
HashMap 扩容 resize() :默认是扩原来的2倍(因为HashMap的大小只能是2次幂),扩的是数组不是链表。
- 1.7版本:
- 先生成长度为原来2倍的新数组
- 遍历老数组中的每个位置上的链表上的每个元素
- 取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
- 头插法将元素添加到新数组中去
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
- 1.8版本:
- 先生成新数组,长度是老数组的2倍
- 遍历老数组中的每个位置上的链表或红黑树
- 如果是链表,则直接将链表中的每个元素里新计算下标,并添加到新数组中去
(将链表重新链接,按照低位区和高位区重新分配到新数组;) - 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置:统计每个下标位置的元素个数,如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置;如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置、
(调用split方法将红黑树重新切分为低位区和高位区2个链表;判断低位区和高位区链表的长度,链表长度小于6,则会进行取消树化的处理,否则会将新生成的链表重新树化;) - 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
- 1.7版本:
Java8为什么将HashMap的插入方法改为了尾插法?
- 头插法,即新插入的元素会插入到链表的头部,会产生以下问题:
- 破坏了链表元素的插入顺序:由于头插法是将新插入的元素插入到链表的头部,这样就导致链表的顺序与元素插入的顺序相反,不利于一些需要按照插入顺序遍历的场景。
- 容易引起链表环形问题:由于头插法需要修改链表头,这会导致在并发环境下,触发resize()时多个线程同时修改新数组的桶节点的链表头,可能会引起链表环形问题,使得链表无法正确遍历或者出现死循环的情况。
??
- 尾插法,即新插入的元素会插入到链表的尾部,这样可以解决很多问题并且有以下优点:。
- 提高查询效率:尾插法使得链表元素的插入顺序与元素插入的顺序一致,从而方便了元素的查找和遍历操作,提高了HashMap的查询效率。
- 避免链表环形问题:尾插法是将新插入的元素插入到链表的尾部,不需要修改链表头,因此可以避免在并发环境下多个线程修改链表头导致的链表环形问题。
- 1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
- Java8及以后的版本插入操作的平均时间要比Java8之前的版本快,差距在1ms左右,这是由于Java8将HashMap的插入方法改为了尾插法,避免了链表环形问题的发生,同时优化了哈希算法和查询效率,从而提高了HashMap的性能。
- 头插法,即新插入的元素会插入到链表的头部,会产生以下问题:
LinkedHashMap?
LinkedHashMap 底层结构是数组+链表+双向链表,实际上它继承了 HashMap,在 HashMap 的基础上维护了一个双向链表
LinkedHashMap 把 HashMap 的 Entry数组 加了两个指针:before 和 after。就是要把塞入的 Entry 之间进行关联,串成双向链表;有了这个双向链表,我们的插入可以是有序的,这里的有序不是指大小有序,而是插入有序。LinkedHashMap在遍历的时候实际用的是双向链表来遍历的,所以LinkedHashMap的大小不会影响到遍历的性能
并且内部还有个 accessOrder 成员,默认是 false, 代表链表是顺序是按插入顺序来排的,如果是 true 则会根据访问顺序来进行调整,就是咱们熟知的 LRU 那种,如果哪个节点访问了,就把它移到最后,代表最近访问的节点。TreeMap?
TreeMap的底层数据结构是红黑树,TreeMap的key不能为null(如果为null,那还怎么排序呢)。
TreeMap有序是通过实现 Comparable 接口或者自定义实现一个 comparator 传入构造函数(如果comparator为null,那么就使用自然顺序 ),这样塞入的节点就会根据你定义的规则进行排序。因此它除了作为 Map 外,还可以用作双端队列。Hashtable、HashMap、TreeMap?
- Hashtable
- 不允许使用 null 键或 null 值。
- 使用场景:1、当需要确保数据的线程安全,且在多线程环境中共享 Map 时,可以考虑使用 Hashtable。2、由于其性能相对较低,推荐在遗留代码中或者特定要求线程安全的小规模数据集合中使用。
- HashMap
- 允许一个 null 键和多个 null 值。
- 使用场景: 1、在非多线程环境中,或者在读多写少的场景下(可以通过外部同步来解决线程安全问题),HashMap 是一个优选,因为它提供了更好的性能。 2、当需要快速查找、插入和删除键值对时,特别是在数据量较大的情况下。
- 当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
- TreeMap
- key不能为null
- 使用场景:1、当需要一个总是保持排序状态的 Map 时,TreeMap 是最合适的选择。它适用于需要频繁地进行有序遍历或范围搜索的场景。 2、 在需要根据键进行排序的应用中,如时间线索引、自然排序的目录结构等。3、 当数据结构的大小频繁变动,且需要保持有序状态时,TreeMap 通常比维护一个 ArrayList 之后再排序要高效。
- Hashtable
ConcurrentHashMap?
- ConcurrentHashMap是线程安全的Map实现类,它在juc包下的。线程安全的Map实现类除了ConcurrentHashMap还有一个叫做Hashtable。当然了,也可以使用Collections来包装出一个线程安全的Map,但无论是Hashtable还是Collections包装出来的都比较低效(因为是直接在外层套synchronize);
HashMap不是线程安全的,多线程环境下有可能会有数据丢失和获取不了最新数据的问题 - ConcurrentHashMap 本质上是一个 HashMap,因此功能和 HashMap 一样,但是ConcurrentHashMap 在 HashMap 的基础上,提供了并发安全的实现。并发安全的主要实现是通过对指定的 Node 节点加锁,来保证数据更新的安全性
- 底层数据结构
JDK1.7底层采用分段的数组+链表实现
JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树 - 加锁的方式
JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
JDK1.8采用CAS添加新节点,(如果已存在节点)采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
- ConcurrentHashMap是线程安全的Map实现类,它在juc包下的。线程安全的Map实现类除了ConcurrentHashMap还有一个叫做Hashtable。当然了,也可以使用Collections来包装出一个线程安全的Map,但无论是Hashtable还是Collections包装出来的都比较低效(因为是直接在外层套synchronize);
红黑树
- Red-Black Tree是一种自平衡的二叉搜索树,它通过一些附加的信息(颜色标记)保持树的平衡。这种平衡性质确保了在最坏情况下对于各种基本动态集合操作(插入、删除、查找)的性能都有较好的上界,保证了树的高度是对数级别的。
- 在Java中,
TreeMap
和TreeSet
类使用红黑树来实现有序映射和有序集合。 - 红黑树具有以下几个特征:
- 节点颜色: 每个节点都带有颜色,可以是红色或黑色。
- 根节点和叶子节点: 根节点和所有叶子节点(NIL节点)都是黑色的。
- 相邻节点颜色: 相邻的节点不能都是红色。也就是说,红色节点不能直接相连,黑色节点可以相连。
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些规则确保了红黑树的关键性质,即任意一条从根到叶子的路径都不会超过最短路径的两倍长。这保证了红黑树在动态插入和删除操作时能够保持相对平衡,从而避免了出现极端不平衡的情况。
- 红黑树插入和删除:都可能导致树失去平衡,因此需要通过旋转和重新着色等操作来维护平衡性质。
- 插入操作:
- 将节点插入: 将新节点插入到红黑树的合适位置,通常是按照二叉搜索树的插入规则。
- 新节点着色为红色: 插入的新节点着色为红色,以便更容易维护平衡性质。
- 重新着色和旋转: 根据父节点、祖父节点、叔叔节点等之间的颜色关系,可能需要进行以下操作:
- 情况1: 如果父节点是黑色,那么不需要做额外操作,树仍然保持平衡。
- 情况2: 如果父节点是红色而叔叔节点也是红色,可以通过颜色翻转来保持平衡。
- 情况3: 如果父节点是红色而叔叔节点是黑色,并且当前节点是父节点的右子节点,可以通过左旋转和右旋转来保持平衡。
- 情况4: 如果父节点是红色而叔叔节点是黑色,并且当前节点是父节点的左子节点,可以通过右旋转来保持平衡。
- 删除操作:
- 执行普通的二叉搜索树删除操作: 将要删除的节点从树中删除,并根据子节点的情况进行适当的替换。
- 重新着色和旋转: 删除操作可能破坏了红黑树的平衡性质,因此可能需要进行以下操作:
- 情况1: 如果删除的节点或替代节点是红色,只需将替代节点着色为黑色。
- 情况2: 如果删除的节点是黑色,而替代节点是红色,可以将替代节点着色为黑色。
- 情况3: 如果删除的节点和替代节点都是黑色,可能需要通过重新着色和旋转来保持平衡。
- 插入操作:
Java 集合使用泛型的好处。
- 可以确保类型安全,避免强制类型转换,提高代码的可读性和可维护性,能够在编译时错误检测。
- “泛型” 意味着编写的代码可以被不同类型的对象所重用。
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,而这并不是最重要的,因为这也只需把底层存储设置Object即可,添加的数据全部都可向上转型为Object。?? 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。?
泛型类
- 泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
- 类型通配符 一般是 使用 ? 代替具体的类型参数。 例如 List<?> 在逻辑上是List, List 等所有 List<具体类型实参>的父类。
1
2
3
4
5
6
7
8
9public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
类型擦除
- Java 中的泛型基本上都是在编译器这个层次来实现的,在生成的字节代码中是不包含泛型中的类型信息的。
使用泛型的时候加上的类型参数,如 List和 List<?> 等类型在编译时会被擦除为原始类型 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 是不可见的。这个过程就称为类型擦除。 - 类型擦除的基本过程是找到用来替换类型参数的具体类,一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。编译器会将代码中的类型参数都替换成具体的类。这种替换的行为是泛型在 Java 中实现的一部分,它确保了泛型代码在运行时不会受到泛型类型信息的影响,从而保持了 Java 的向后兼容性。
1
2
3
4
5
6
7
8
9
10
11
12// 有一个泛型类或接口如下
public class MyClass<T> {
}
// 在类型擦除之后,它等价于
public class MyClass {
}
// 对于泛型方法:
public <T> void myMethod(T item) {
}
// 在类型擦除后,它等价于
public void myMethod(Object item) {
}
- Java 中的泛型基本上都是在编译器这个层次来实现的,在生成的字节代码中是不包含泛型中的类型信息的。
JVM
“Java 跨平台”
因为有 JVM 屏蔽了底层操作系统。Java源代码会被编译为class文件,class文件运行在JVM之上。JVM是面向操作系统的,它负责把Class字节码解释成系统所能识别的指令并执行,同时也负责程序运行时内存的管理。安装JDK的时可以发现JDK是分「不同的操作系统」,JDK里是包含JVM的,所以Java依赖着JVM实现了『跨平台』JVM 组成。
java虚拟机是jdk的一个部分,有四大组成部分。
1、类加载器:将class字节码文件中的内容加载到内存中。
2、运行时数据区域:负责管理JVM 使用到的内存,比如创建对象和销毁对象。
3、执行引擎:将字节码文件中的指令解释成机器码,同时使用即时编译器优化性能,用gc回收内存。
4、本地接口:调用本地已经编译好的方法(不在字节码文件中)比如虚拟机中提供的cpp方法。
而 JVM 的内存结构,往往指的就是JVM定义的「运行时数据区域」。简单来说就分为了5大块:方法区、堆、程序计数器、虚拟机栈、本地方法栈。其中线程共享:堆、方法区,私有:虚拟机栈、程序计数器、本地方法栈。JVM 参数:
- 通用参数:
-version
:显示Java版本信息。-help
或-?
:显示命令行选项和使用信息。 - 堆相关参数:
-Xms<size>
:设置初始堆大小。-Xmx<size>
:设置最大堆大小。-Xmn<size>
:设置年轻代的大小。
-XX:MaxPermSize=<size>
:设置持久代(Java 8之前)的最大大小。--XX:MaxMetaspaceSize=<size>
:设置元空间(Java 8及更高版本)的最大大小。 - 垃圾回收相关参数: -
-XX:+UseSerialGC
:使用串行垃圾回收器。 --XX:+UseParallelGC
:使用并行垃圾回收器。
-XX:+UseConcMarkSweepGC
:使用CMS垃圾回收器。-XX:+UseG1GC
:使用G1回收器。-XX:+UseZGC
:使用ZGC - 性能调优参数:
-XX:ThreadStackSize=<size>
:设置线程堆栈大小。
-XX:CompileThreshold=<threshold>
:设置方法调用的编译阈值。
-XX:MaxGCPauseMillis=<milliseconds>
:设置垃圾回收最大停顿时间目标。 - 调试参数:
-Xdebug
:开启远程调试。-Xrunjdwp:transport=dt_socket,address=<address>,server=y,suspend=n
:配置JDWP远程调试。-verbose:gc
:输出垃圾回收详细信息。 - 应用程序性能分析参数: -
-javaagent:<path-to-agent-jar>
:启用Java代理,例如用于性能分析工具。
- 通用参数:
Java 编译到执行的过程?
- Java源码到执行的过程,从JVM的角度看可以总结为四个步骤:编译->加载->解释->执行
- 「编译」java源代码经过 语法分析、语义分析、注解处理 最后才生成会class文件。对泛型的擦除和Lombok就在编译阶段。
- 「加载」又可以细分步骤为:装载->连接->初始化。装载则把class文件装载至JVM,连接则校验class信息、分配内存空间及赋默认值,初始化则为变量赋值为正确的初始值。连接里又可以细化为:验证、准备、解析
- 「装配」阶段可以总结为:查找并加载类的二进制数据,在JVM「堆」中创建一个java.lang.Class类的对象,并将类相关的信息存储在JVM「方法区」中
- 【装载时机】为了节省内存的开销,并不会一次性把所有的类都装载至JVM,而是等到「有需要」的时候才进行装载(比如new和反射等等)
- 【装载发生】class文件是通过「类加载器」装载到jvm中的,为了防止内存中出现多份同样的字节码,使用了双亲委派机制
- 【装载规则】JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader)实现装载,而程序中的类文件则由系统加载器(AppClassLoader)实现装载。
- 「连接」这个阶段它做的事情可以总结为:对class的信息进行验证、为「类变量」分配内存空间并对其赋默认值。又可以细化为几个步骤:
- 验证:验证类是否符合 Java 规范和 JVM 规范
- 准备:为类的静态变量分配内存,初始化为系统的初始值
- 解析:将符号引用转为直接引用的过程
- 「初始化」阶段可以总结为:为类的静态变量赋予正确的初始值。过程大概就是收集class的静态变量、静态代码块、静态方法至()方法,随后从上往下开始执行。如果「实例化对象」则会调用方法对实例变量进行初始化,并执行对应的构造方法内的代码。
- 「装配」阶段可以总结为:查找并加载类的二进制数据,在JVM「堆」中创建一个java.lang.Class类的对象,并将类相关的信息存储在JVM「方法区」中
- 「解释」则是把字节码转换成操作系统可识别的执行指令,在JVM中会有字节码解释器和即时编译器。在解释时会对代码进行分析,查看是否为「热点代码」,如果为「热点代码」则触发JIT编译,下次执行时就无需重复进行解释,提高解释速度
- JVM会对「热点代码」做编译,非热点代码直接进行解释。当JVM发现某个方法或代码块的运行特别频繁的时候,就有可能把这部分代码认定为「热点代码」。
- 使用「热点探测」来检测是否为热点代码。「热点探测」一般有两种方式,计数器和抽样。HotSpot使用的是「计数器」的方式进行探测,为每个方法准备了两类计数器:方法调用计数器和回边计数器。这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。即时编译器把热点方法的指令码保存起来,下次执行的时候就无需重复的进行解释,直接执行缓存的机器语言。
- 「执行」操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令。
类加载器、、
- JDK自带有三个类加载器:Bootstrap ClassLoader、ExtClassLoader、AppClassLoader;
JDK 中的本地方法类一般由根加载器(Bootstrp)装载,内部实现的扩展类一般由扩展加载器(Ext)装载,而程序中的类文件则由系统加载器(AppClassLoader)实现装载。- BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA HOME%lib下的jar包和class文件
- ExtClassLoader是AppClassLoader的父类加载器,负贵加载%JAVA HOME%/lib/ext文件夹下的jar包和class类。
- AppClassLoader是自定义类加载器的父类,负贵加载classpath下的类文件。
- JDK自带有三个类加载器:Bootstrap ClassLoader、ExtClassLoader、AppClassLoader;
双亲委派模型
- class文件是通过「类加载器」装载至JVM中的,为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向上)
- JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,内部实现的扩展类一般由扩展加载器(ExtClassLoader)实现装载,而程序中的类文件则由系统加载器(AppClassLoader)实现装载。
AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。这里的父子关系并不是通过继承实现的,而是组合。 - 打破双亲委派机制:自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)
- 打破双亲委派机制案例:Tomcat
- 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器
- 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载
- 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离和加载Tomcat本身的类
- 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器
- ShareClassLoader、CatalinaClassLoader、CommonClassLoader的目录可以在Tomcat的catalina.properties进行配置
- ???线程上下文加载器:由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。
JVM的内存结构?
- .class文件会被类加载器装载至JVM中,并且JVM会负责程序「运行时」的「内存管理」。而JVM的内存结构,往往指的就是JVM定义的「运行时数据区域」。简单来说就分为了5大块:方法区、堆、程序计数器、虚拟机栈、本地方法栈
- 要值得注意的是:这是JVM「规范」的分区概念,到具体的实现落地,不同的厂商实现可能是有所区别的。
- 程序计数器:线程切换意味着「中断」和「恢复」,那自然就需要有一块区域来保存「当前线程的执行信息」。所以,程序计数器就是用于记录各个线程执行的字节码的地址(分支、循环、跳转、异常、线程恢复等都依赖于计数器)
- 虚拟机栈:每个线程在创建的时候都会创建一个「虚拟机栈」,每次方法调用都会创建一个「栈帧」。每个「栈帧」会包含几块内容:局部变量表、操作数栈、动态连接和返回地址。它的作用:它保存方法的局部变量、部分变量的计算并参与了方法的调用和返回。
- 本地方法栈:本地方法栈跟虚拟机栈的功能类似,虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。这里的「本地方法」指的是「非Java方法」,一般本地方法是使用C语言实现的。
- 方法区:在HotSpot虚拟机,就会常常提到「永久代」这个词,jdk8 前用「永久代」实现了「方法区」,而很多其他厂商虚拟机其实是没有「永久代」的概念的。JDK8中,已经用「元空间」来替代了「永久代」作为「方法区」的实现了。
- 方法区主要是用来存放已被虚拟机加载的「类相关信息」:包括类信息、常量池。类信息又包括了类的版本、字段、方法、接口和父类等信息;常量池又可以分「静态常量池」和「运行时常量池」
- 静态常量池主要存储的是「字面量」以及「符号引用」等信息,也包括了我们说的「字符串常量池」。
- 运行时常量池存储的是类加载时生成的「直接引用」等信息。
- 从逻辑分区的角度而言「常量池」是属于「方法区」的。但自从在 jdk7 以后,就已经把运行时常量池和静态常量池转移到了堆内存中进行存储(对于物理分区来说运行时常量池和静态常量池就属于堆)
- 从 jdk8 已经把「方法区」的实现从「永久代」变成「元空间」,有什么区别?
最主要的区别就是:元空间存储不在虚拟机中,而是使用本地内存,JVM 不会再出现方法区的内存溢出,以往「永久代」经常因为内存不够用导致跑出OOM异常。按JDK8版本,总结起来其实就相当于:「类信息」是存储在「元空间」的(也有人把类信息这块叫做类信息常量池,主要是叫法不同,意思到位就好),而「常量池」从JDK7开始,从物理存储角度上就在「堆」中。
- 方法区主要是用来存放已被虚拟机加载的「类相关信息」:包括类信息、常量池。类信息又包括了类的版本、字段、方法、接口和父类等信息;常量池又可以分「静态常量池」和「运行时常量池」
- 堆:堆是线程共享的区域,几乎类的实例和数组分配的内存都来自于它。堆被划分为「新生代」和「老年代」,「新生代」又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。将堆内存分开了几块区域,主要跟内存回收有关(垃圾回收机制)
jvm线程共享区?
线程共享:堆区和方法区
线程独有:栈、本地方法栈和程序计数器JVM内存结构和Java内存模型有啥区别?
没有啥直接关联,,Java内存模型是跟「并发」相关的,它是为了屏蔽底层细节而提出的规范,希望在上层(Java层面上)在操作内存时在不同的平台上也有相同的效果;JVM内存结构(又称为运行时数据区域),它描述着当我们的class文件加载至虚拟机后,各个分区的「逻辑结构」是如何的,每个分区承担着什么作用。Java对象创建过程?new一个对象的步骤?
- 检查类符号引用:首先,JVM 会检查 new 关键字后面的类符号引用,确保在常量池中能找到对应的类。
- 加载类:如果在常量池中找到了类符号引用,JVM 就会加载这个类,包括加载、连接(验证、准备、解析)和初始化阶段。(如果已经加载了这个类,那么类信息可以在方法区中找到)
- 分配内存:JVM 会根据类的定义,在堆内存中为对象分配内存空间。这个内存空间包括对象的所有成员变量。这个过程包括选择内存分配方式(如指针碰撞、空闲列表、TLAB)、分配内存并进行内存清零。
- 初始化对象:分配完内存空间后,JVM 会将对象的除对象头外的内存空间初始化为默认值(基本数据类型为 0,引用类型为 null)。最后,JVM 会设置对象的对象头,包括哈希码、GC 信息等元信息。
- 调用构造方法:执行对象的初始化逻辑,包括对成员变量进行赋值、执行一些初始化操作等。
- 返回对象引用:最后,new 操作符会返回一个指向新创建对象的引用,通过这个引用可以在程序中操作对象的属性和调用对象的方法。
- 这些步骤是创建一个对象的基本流程,无论是通过 new 关键字创建对象,还是通过反射、序列化等方式创建对象,都要经历这些步骤。
简述Java的对象结构(JMM??)
- Java对象由三个部分组成:对象头、实例数据、对齐填充。
- 对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
- 实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
- 对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
- Java对象由三个部分组成:对象头、实例数据、对齐填充。
程序员可以根据需要控制JVM在特定时间进行垃圾回收吗?
在大多数情况下,不能直接控制 JVM 在特定时间进行垃圾回收。垃圾回收是 JVM 的自动内存管理系统的一部分,其目标是在运行时自动回收不再使用的内存,而不需要程序员显式干预。但是,可以通过一些 JVM 参数和 API 间接地影响垃圾回收的行为。java的对象是怎么被回收的?
- 什么是垃圾:只要对象不再被使用,那即是垃圾。GC就是对堆中的对象回收。
- 如何判断为垃圾:常用的算法有两个「引用计数法」和「可达性分析法」;
- JVM使用的是可达性分析算法「GC Roots」:GC Roots是一组必须活跃的引用,跟GC Roots无关联的引用即是垃圾,可被回收
- 常见的垃圾回收算法:标记清除、标记复制、标记整理。整理算法是前两者的折中:未必要有一块「大的完整空间」才能解决内存碎片的问题,我只要能在「当前区域」内进行移动,把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛
- 分代:堆分了「新生代」和「老年代」,「新生代」又分为「Eden」和「Survivor」区,「Survivor」区又分为「From Survivor」和「To Survivor」区
堆内存占比:年轻代占堆内存1/3,老年代占堆内存2/3。Eden区占年轻代8/10,Survivor区占年轻代2/10(其中From 和To 各站1/10) - 为什么需要分代:大部分对象都死得早,只有少部分对象会存活很长时间。在堆内存上都会在物理或逻辑上进行分代,为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率。
- 垃圾回收器:实际上就是「实现了」垃圾回收算法(标记复制、标记整理以及标记清除算法)
「年轻代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew
「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS - Minor GC:当Eden区满了则触发,从GC Roots往下遍历,年轻代GC不关心老年代对象
- 什么是card table【卡表】:空间换时间(类似bitmap),能够避免扫描老年代的所有对应进而顺利进行Minor GC案例:老年代对象持有年轻代对象引用)
- Full GC:。。。
如何判断对象可以被回收判断?
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
- 可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
JVM有哪些垃圾回收算法?
- 标记清除算法:a.标记阶段:把垃圾内存标记出来;b.清除阶段:直接将垃圾内存回收。
这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。 - 复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。
- 标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。
- 标记清除算法:a.标记阶段:把垃圾内存标记出来;b.清除阶段:直接将垃圾内存回收。
JVM 分代回收、、
- 堆的区域划分
1、堆被分为了两份:「新生代」和「老年代」【1:2】
2、对于新生代,内部又被分为了「Eden」和「Survivor」区,「survivor」区又分为「From」和「To」区【8:1:1】 - 对象回收分代回收策略
- 新创建的对象,都会先分配到eden区
- 当伊甸园内存不足,标记伊甸园与from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和from 内存都得到释放
- 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将其复制到from区
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
- MinorGC、Mixed Gc、FullGC的区别是什么
- MinorGC【young GC】(新生代垃圾回收):发生在新生代的垃圾回收,暂停时间短(STW)
- Mixed GC(混合回收):新生代+老年代部分区域的垃圾回收,G1收集器特有
- FullGC(老年代垃圾回收):新生代+老年代完整垃圾回收,暂停时间长(STW),应尽力避免
- Minor GC、Mixed GC 和 Full GC 的触发条件如下:
- Minor GC:在新对象创建时,当 Eden 区满时触发 Minor GC。通常情况下,Minor GC 会回收年轻代的 Eden 区和 Survivor 区中的垃圾对象。
- Mixed GC:在 G1 垃圾回收器中,Mixed GC 是指同时执行部分 Young GC 和部分 Old GC 的过程;在一次 Full GC 后,可能会触发 Mixed GC 来对部分老年代和年轻代进行回收。
- Full GC:在老年代空间不足、永久代空间不足(如果使用永久代)、老年代连续多次触发 Minor GC 无法回收足够的空间时,或者明确调用 System.gc() 方法时,可能会触发 Full GC。
- 堆的区域划分
CMS垃圾收集器
- CMS的全称:Concurrent Mark Sweep,翻译过来是「并发标记清除」
对比其他垃圾收集器(Seria和Parallel和parNew),它最大的不同点就是“并发”:在GC线程工作的时候,用户线程不会完全停止,用户线程在“部分场景下”与GC线程一起并发执行,避免老年代 GC出现长时间的卡顿(Stop The World) - CMS可简单分为5个步骤:初始标记、并发标记、并发预清理、重新标记及并发清除,即CMS主要是实现了「标记清除」GC
- 「初始标记」会标记GCRoots「直接关联」的对象以及「年轻代」指向「老年代」的对象。这个过程是会发生Stop The World的。但这个阶段的速度算是很快的,因为没有「向下追溯」(只标记一层)
- 「并发标记」这个过程是不会停止用户线程的(不会发生 Stop The World)。这一阶段主要是从GC Roots向下「追溯」,标记所有可达的对象;在GC的角度而言,是比较耗费时间的(需要追溯)
- 「并发预处理」这个阶段主要想干的事情:希望能减少下一个阶段「重新标记」所消耗的时间,因为下一个阶段「重新标记」是需要Stop The World的
这个阶段会扫描可能由于「并发标记」时导致老年代发生变化的对象(「并发标记」这个阶段由于用户线程是没有被挂起的,可能有些对象从新生代晋升到了老年代,可能有些大对象直接分配到了老年代,可能老年代或者新生代的对象引用发生了变化…),会再扫描一遍标记为dirty的卡页
针对老年代的对象,其实还是可以借助类card table的存储(将老年代对象发生变化所对应的卡页标记为dirty);对于新生代的对象,我们还是得遍历新生代来看看在并发标记过程中有没有对象引用了老年代.. - 「重新标记」阶段会Stop The World,这个过程的停顿时间其实很大程度上取决于上面「并发预处理」阶段(可以发现,这是一个追赶的过程:一边在标记存活对象,一边用户线程在执行产生垃圾)
- 最后就是「并发清除」阶段,不会Stop The World,一边用户线程在执行,一边GC线程在回收不可达的对象。这个过程,还是有可能用户线程在不断产生垃圾,但只能留到下一次GC 进行处理了,产生的这些垃圾被叫做“浮动垃圾”,完了以后会重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备
- 比起G1,CMS有什么缺点呢?
- 空间需要预留:CMS垃圾收集器可以一边回收垃圾,一边处理用户线程,那需要在这个过程中保证有充足的内存空间供用户使用。如果CMS运行过程中预留的空间不够用了,会报错(Concurrent Mode Failure),这时会启动 Serial Old垃圾收集器进行老年代的垃圾回收,会导致停顿的时间很长。
- 内存碎片问题:CMS本质上是实现了「标记清除算法」的收集器(从过程就可以看得出),这会意味着会产生内存碎片。由于碎片太多,又可能会导致内存空间不足所触发full GC,CMS一般会在触发full GC这个过程对碎片进行整理。
- 要处理内存碎片的问题(整理),整理涉及到「移动」/「标记」,那这个过程肯定会Stop The World的,如果内存足够大(意味着可能装载的对象足够多),那这个过程卡顿也是需要一定的时间的。
- CMS的全称:Concurrent Mark Sweep,翻译过来是「并发标记清除」
G1垃圾收集器 https://www.bilibili.com/read/cv26352521/?spm_id_from=333.999.0.0&jump_opus=1
- 在G1垃圾收集器的世界中,堆的划分不再是物理形式,而是以”逻辑”的形式进行划分;
使用CSet来存储可回收Region的集合,使用RSet来处理跨代引用的问题(注意:RSet不保留 年轻代相关的引用关系)
G1垃圾收集器世界的「堆」空间分布:
从图上就可以发现,堆被划分了多个同等份的区域 Region
不过像之前的「分代」概念在G1的世界还是一样奏效的,比如说:新对象一般会分配到Eden区、经过默认15次的Minor GC新生代的对象如果还存活,会移交到老年代等等…老年代、新生代、Survivor这些规则是跟CMS一样的,此外,G1中还有一种叫 Humongous(大对象)区域,其实就是用来存储特别大的对象(大于Region内存的一半),一旦发现没有引用指向大对象,就可直接在年轻代的Minor GC中被回收掉 - 为什么要将堆空间进行细分多个小的区域?
像以前的垃圾收集器都是对堆进行「物理」划分,如果堆空间(内存)大的时候,每次进行「垃圾回收」都需要对一整块大的区域进行回收,那收集的时间是不好控制的。而划分多个小区域之后,那对这些「小区域」回收就容易控制「收集时间」了 - GC过程:在G1收集器中可以主要分为有Minor GC(Young GC) 和 Mixed GC,Full GC
- 【Eden区满则触发】Minor GC 回收过程可简单分为:(STW) 扫描 GC Roots、更新&&处理Rset、复制清除
- 【整堆空间占一定比例则触发】Mixed GC 依赖「全局并发标记」,得到CSet(可回收Region),就进行「复制清除」
- 【也有些特殊场景可能会发生】Full GC
- Minor GC:
- 触发时机跟前面提到过的垃圾收集器都是一样的
等到Eden区满了之后,会触发Minor GC。Minor GC同样也是会发生Stop The World的
要补充说明的是:在G1的世界里,新生代和老年代所占堆的空间是没那么固定的(会动态根据「最大停顿时间」进行调整)
这块要知道会给我们提供参数进行配置就好了
所以,动态地改变年轻代Region的个数可以「控制」Minor GC的开销 - 回收过程,可以简单分为为三个步骤:根扫描、更新&&处理 RSet、复制对象
第一步应该很好理解,因为这跟之前CMS是类似的,可以理解为初始标记的过程
第二步涉及到「Rset」的概念
从上一次我们聊CMS回收过程的时候,同样讲到了Minor GC,它是通过「卡表」(cart table)来避免全表扫描老年代的对象
因为Minor GC 是回收年轻代的对象,但如果老年代有对象引用着年轻代,那这些被老年代引用的对象也不能回收掉
同样的,在G1也有这种问题(毕竟是Minor GC)。CMS是卡表,而G1解决「跨代引用」的问题的存储一般叫做RSet
只要记住,RSet这种存储在每个Region都会有,它记录着「其他Region引用了当前Region的对象关系」
对于年轻代的Region,它的RSet 只保存了来自老年代的引用(因为年轻代的没必要存储啊,自己都要做Minor GC了)
而对于老年代的 Region 来说,它的 RSet 也只会保存老年代对它的引用(在G1垃圾收集器,老年代回收之前,都会先对年轻代进行回收,所以没必要保存年轻代的引用)
那第二步看完RSet的概念,应该也好理解了吧?
无非就是处理RSet的信息并且扫描,将老年代对象持有年轻代对象的相关引用都加入到GC Roots下,避免被回收掉
第三步也挺好理解的:把扫描之后存活的对象往「空的Survivor区」或者「老年代」存放,其他的Eden区进行清除
- 触发时机跟前面提到过的垃圾收集器都是一样的
- Mixed GC:会选定所有的年轻代Region,部分「回收价值高」的老年代Region(回收价值高其实就是垃圾多)进行采集
- Full GC
- 在G1垃圾收集器的世界中,堆的划分不再是物理形式,而是以”逻辑”的形式进行划分;
JVM调优到底是干啥的?
- 我们一般优化系统的思路是这样的:
- 一般来说关系型数据库是先到瓶颈,首先排查是否为数据库的问题(这个过程中就需要评估自己建的索引是否合理、是否需要引入分布式缓存、是否需要分库分表等等)
- 然后,我们会考虑是否需要扩容(横向和纵向都会考虑)(这个过程中我们会怀疑是系统的压力过大或者是系统的硬件能力不足导致系统频繁出现问题)
- 接着,应用代码层面上排查并优化(扩容是不能无止境的,里头里外都是钱阿。这个过程中我们会审视自己写的代码是否存在资源浪费的问题,又或者是在逻辑上可存在优化的地方,比如说通过并行的方式处理某些请求)
- 再接着,JVM层面上排查并优化(审视完代码之后,这个过程我们观察JVM是否存在多次GC问题等等)
- 最后,网络和操作系统层面排查(这个过程查看内存/CPU/网络/硬盘读写指标是否正常等等)
- 绝大多数情况下到第三步就结束了,一般经过「运维团队」给我们设置的JVM和机器上的参数已经满足绝大多数的需求了。
- 在我的理解下,调优JVM其实就是在「理解」JVM内存结构以及各种垃圾收集器前提下,结合自己的现有的业务来「调整参数」,使自己的应用能够正常稳定运行。一般调优JVM我们认为会有几种指标可以参考:『吞吐量』、『停顿时间』和『垃圾回收频率』。基于这些指标,我们就有可能需要调整:
- 内存区域大小以及相关策略(比如整块堆内存占多少、新生代占多少、老年代占多少、Survivor占多少、晋升老年代的条件等等)比如(-Xmx:设置堆的最大值、-Xms:设置堆的初始值、-Xmn:表示年轻代的大小、-XX:SurvivorRatio:伊甸区和幸存区的比例等等)(按经验来说:IO密集型的可以稍微把「年轻代」空间加大些,因为大多数对象都是在年轻代就会灭亡。内存计算密集型的可以稍微把「老年代」空间加大些,对象存活时间会更长些)
- 垃圾回收器(选择合适的垃圾回收器,以及各个垃圾回收器的各种调优参数)比如(-XX:+UseG1GC:指定 JVM 使用的垃圾回收器为 G1、-XX:MaxGCPauseMillis:设置目标停顿时间、-XX:InitiatingHeapOccupancyPercent:当整个堆内存使用达到一定比例,全局并发标记阶段 就会被启动等等)
没错,这些都是因地制宜,具体问题具体分析(前提是得懂JVM的各种基础知识,基础知识都不懂,谈何调优)
- 一般我们是「遇到问题」之后才进行调优的,而遇到问题后需要利用各种的「工具」进行排查
- 通过jps命令查看Java进程「基础」信息(进程号、主类)。这个命令很常用的就是用来看当前服务器有多少Java进程在运行,它们的进程号和加载主类是啥
- 通过jstat命令查看Java进程「统计类」相关的信息(类加载、编译相关信息统计,各个内存区域GC概况和统计)。这个命令很常用于看GC的情况
- 通过jinfo命令来查看和调整Java进程的「运行参数」。
- 通过jmap命令来查看Java进程的「内存信息」。这个命令很常用于把JVM内存信息dump到文件,然后再用MAT( Memory Analyzer tool 内存解析工具)把文件进行分析
- 通过jstack命令来查看JVM「线程信息」。这个命令用常用语排查死锁相关的问题
- 还有近期比较热门的Arthas(阿里开源的诊断工具),涵盖了上面很多命令的功能且自带图形化界面。
- JVM 的 JIT 优化技术:比较出名的有两种:方法内联和逃逸分析
所谓方法内联就是把「目标方法」的代码复制到「调用的方法」中,避免发生真实的方法调用
因为每次方法调用都会生成栈帧(压栈出栈记录方法调用位置等等)会带来一定的性能损耗,所以「方法内联」的优化可以提高一定的性能。在JVM中也有相关的参数给予我们指定(-XX:MaxFreqInlineSize、-XX:MaxInlineSize)
而「逃逸分析」则是判断一个对象是否被外部方法引用或外部线程访问的分析技术,如果「没有被引用」,就可以对其进行优化,比如说:- 锁消除(同步忽略):该对象只在方法内部被访问,不会被别的地方引用,那么就一定是线程安全的,可以把锁相关的代码给忽略掉
- 栈上分配:该对象只会在方法内部被访问,直接将对象分配在「栈」中(Java默认是将对象分配在「堆」中,是需要通过JVM垃圾回收期进行回收,需要损耗一定的性能,而栈内分配则快很多)
- 标量替换/分离对象:当程序真正执行的时候可以不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了
- 我们一般优化系统的思路是这样的:
有没有排查过线上 oom 的问题?
- 没有,但是,知道。。。
OOM 是 out of memory 的简称,表示程序需要的内存空间大于 JVM 分配的内存空间。OOM 后果就是导致程序崩溃;可以通俗理解:程序申请内存过大,虚拟机无法满足。 - 导致 OOM 错误的情况一般是:
1、给 JVM 虚拟机分配的内存太小,实际业务需求对内存的消耗比较多
2、Java 应用里面存在内存泄漏的问题,或者应用中有大量占用内存的对象,并且没办法及时释放。我给大家解释一下内存泄漏和内存溢出是两个完全不一样的情况
内存泄露:申请使用完的内存没有释放,导致虚拟机不机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分机分配给别人用。
内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出。 - 常见的 OOM 异常情况有两种:
java.lang.OutOfMemoryError: Java heap space ——>java 堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx 来修改。
java.lang.OutOfMemoryError: PermGen space 或java.lang.OutOfMemoryError:MetaSpace ——>java 方法区,溢出了,一般出现在大量 Class、或者采用 cglib 等反射机制的情况,因为这些情况会产生大量的 Class 信息存储于方法区。这种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m-XX:MaxPermSize=256m 的形式修改。 - 另外,过多的常量尤其是字符串也会导致方法区溢出。
遇到这类问题,通常的排查方式是,先获取内存的 Dump 文件。Dump 文件有两种方式来生成:第一种是配置 JVM 启动参数,当触发了 OOM 异常的时候自动生成;第二种是使用 jmap 工具来生成。
然后使用 MAT 工具来分析 Dump 文件。如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。掌握了泄漏对象的类信息和 GC Roots 引用链的信息,就可以比较准确地定位泄漏代码的位置。如果是普通的内存溢出,确实有很多占用内存的对象,那就只需要提升堆内存空间即可。
- 没有,但是,知道。。。
Java 中会存在内存泄漏吗,请简单描述。
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题( 这也是 Java 被广泛使用于服务器端编程的一个重要原因 );然而在实际开发中,可能会存在无用但可达的对 象,这些对象 不能被 GC 回收 ,因此也会导致内存泄露的发生 。
JDBC
JDBC、Spring DAO、MyBatis?
Java Database Connectivity、Spring Data Access Object和 MyBatis 是 Java 中用于数据库访问的三种不同的技术或模式。- JDBC 是 Java 提供的标准数据库访问接口,它允许 Java 应用程序与不同的关系型数据库进行通信。JDBC 提供了一组 API,通过这些 API,开发者可以执行 SQL 查询、更新数据库、处理事务等操作。JDBC 是直接与数据库进行交互的底层技术,需要开发者编写较多的代码来处理数据库连接、SQL 执行和结果集处理等细节。
- Spring DAO 是 Spring 框架中的一个模块,它提供了一种高层次的、面向对象的数据库访问方式,通过封装底层的 JDBC 操作,简化了数据库访问的代码。Spring DAO 的目标是提供更高级别的抽象,使得开发者可以更专注于业务逻辑而不用过多关心数据库访问的细节。Spring DAO 提供了对声明式事务、异常处理等特性的支持。
- MyBatis 是一种基于 Java 的持久层框架,它提供了一种将 SQL 语句与 Java 对象进行映射的方式,通过 XML 或注解配置 SQL 映射关系。MyBatis 避免了手动编写大量 JDBC 代码,同时提供了更灵活的 SQL 控制和结果集映射。MyBatis 是一种半自动化的持久层框架,它允许开发者直接编写 SQL,但提供了一些便利的功能来简化数据库访问。
- 关系总结:
- JDBC 是直接与数据库进行交互的底层技术,需要开发者编写更多的数据库访问相关代码。
- Spring DAO 是 Spring 框架中的一部分,提供了更高层次的抽象,简化了数据库访问的代码,提供声明式事务等功能。
- MyBatis 是一种基于 Java 的持久层框架,通过将 SQL 语句与 Java 对象进行映射,简化了数据库访问的代码,提供了更灵活的 SQL 控制和结果集映射。
在实际应用中,Spring DAO 和 MyBatis 可以与 JDBC 结合使用,以便在不同层次上提供更多的抽象和功能,同时使开发更加方便。例如,可以使用 MyBatis 提供的 SQL 映射和结果集映射功能,与 Spring DAO 集成,同时利用 Spring 提供的声明式事务管理。
JDBC操作的步骤:
加载数据库驱动类
打开数据库连接
执行sql语句
处理返回结果
关闭资源JDBC事务的步骤:
- 连接数据库: 使用
DriverManager
获取数据库连接。这涉及提供数据库的连接字符串、用户名和密码等信息。1
Connection connection = DriverManager.getConnection("jdbc:dbms://localhost:3306/mydatabase", "username", "password");
- 关闭自动提交: 默认情况下,每个 JDBC 连接都是自动提交的,即每个 SQL 语句都被立即执行并提交到数据库。在事务中,我们通常关闭自动提交。
- 执行事务操作: 在事务中,执行一系列的 SQL 操作,包括插入、更新等。这些操作将被延迟提交,直到显式调用
commit()
1
2Statement statement = connection.createStatement();
statement.executeUpdate("INSERT INTO mytable (column1, column2) VALUES ('value1', 'value2')"); - 事务回滚: 如果在事务执行过程中发生错误或某些条件不满足,可以调用
rollback()
方法将事务回滚到开始状态,撤销之前的所有更改。 - 提交事务: 如果事务的执行没有发生错误,且符合预期,可以调用
commit()
方法将事务中的所有更改提交到数据库。 - 关闭连接: 当事务完成后,需要关闭数据库连接。关闭连接将释放资源并结束与数据库的通信。
1
2
3
4
5
6
7
8
9
10
11
12// 整个事务的执行可以通过 try-catch 块进行异常处理,以确保在发生错误时能够执行回滚操作
try {
connection.setAutoCommit(false);
// 执行事务操作
// ...
connection.commit();
} catch (SQLException e) {
connection.rollback();
e.printStackTrace();
} finally {
connection.close();
}
- 连接数据库: 使用
在使用jdbc的时候,如何防止出现sql注入的问题?
使用PreparedStatement类,而不是使用Statement类怎么在JDBC内调用一个存储过程
使用CallableStatement是否了解连接池,使用连接池有什么好处?
数据库连接是非常消耗资源的,影响到程序的性能指标。连接池是用来分配、管理、释放数据库连接的,可以使应用程序重复使用同一个数据库连接,而不是每次都创建一个新的数据库连接。通过释放空闲时间较长的数据库连接,避免数据库因为创建太多的连接而造成的连接遗漏问题,提高了程序性能。你所了解的数据源技术有那些?使用数据源有什么好处?
Dbcp,c3p0等,用的最多还是c3p0,因为c3p0比dbcp更加稳定,安全;通过配置文件的形式来维护数据库信息,而不是通过硬编码。当连接的数据库信息发生改变时,不需要再更改程序代码就实现了数据库信息的更新。
Java并发
并发、并行、串行之间的区别
1,串行:一个任务执行完,才能执行下一个任务
2,并行(Parallelism):多核CPU下,多个任务同时执行
3,并发(Concurency):多核CPU下,多个线程轮流使用一个或多个CPU。两个任务整体看上去是同时执行,在底层,两个任务被拆成了很多份,然后一个一个执行,站在更高的度看来两个任务是同时在执行的进程与线程的区别。
- 线程作为最小调度单位,进程作为资源分配的最小单位。
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
怎么理解Java多线程? https://www.bilibili.com/read/cv22973356/?spm_id_from=333.999.0.0&jump_opus=1
创建线程的方式有哪些?
继承Thread类
实现runnable接口
实现Callable接口
线程池创建线程(项目中使用方式)runnable 和 callable 有什么区别
Runnable 接口run方法没有返回值
Callable接口call方法有返回值,需要FutureTask获取结果
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛线程包括哪些状态?
Thread中的6个枚举State:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、时间等待(TIMED_WALTING)、终止(TERMINATED)线程状态之间是如何变化的?
- 创建线程对象是新建状态
- 调用了start()方法转变为可执行状态(还要获取到CPU执行权才能运行)
- 线程获取到了CPU的执行权,执行结束是终止状态
- 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
- 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
Java 线程开启、终止、同步的方式
- 开启线程:
- 继承 Thread 类并重写
run
方法,然后创建线程对象并调用start
方法启动线程。 - 实现 Runnable 接口并实例化一个
Runnable
对象,然后通过 Thread 类的构造方法将 Runnable 对象传递给线程,调用 start方法启动。 run()
和start()
有什么区别?
start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。只能被调用一次。
run():封装了要被线程执行的代码,可以被调用多次,
- 继承 Thread 类并重写
- 线程终止:
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止??用
volatile
标记变量控制线程的执行状态 - 使用stop方法强行终止(不推荐,方法已作废),可能会导致线程状态不一致或资源未正确释放。
- 使用interrupt方法中断线程。这种方式会向线程发送一个中断信号,线程可以在适当的时候检查中断标志并做出响应,安全地停止线程。
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止??用
- 线程同步
- wait():
Object
的成员方法,每个对象都有;wait(long)和 wait() 可以被 notify 唤醒,wait() 如果不唤醒就一直等下去- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)
- sleep() :
Thread
类的静态方法,用于让当前线程暂停执行一段时间(以毫秒为单位),等待指定时间后重新进入就绪状态,不会停止线程。- sleep() 会使当前线程进入阻塞状态,让出 CPU 资源给其他线程使用;如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
- 到时间后该线程会重新变为可运行状态,进入就绪队列中等待 CPU 调度器重新分配 CPU 时间片来执行(“就绪队列”是指操作系统内核层面的调度队列,和非公平锁中的等待队列的概念没有太大关系。。)
- yield() :Thread 类的静态方法,用于让当前线程让出 CPU 时间片,使得其他具有相同优先级的线程有机会执行。不会停止线程,而是让线程重新进入就绪状态,等待调度器重新分配 CPU 时间片。
- join():设置当前线程,等待另一线程结束后运行
- notify() :是用于线程间通信的方法,它用于唤醒一个正在等待对象监视器的线程。不会停止线程,而是唤醒wait()线程,使其继续执行。
- wait():
- 开启线程:
Java内存模型? Java3y https://www.bilibili.com/read/cv24200309/?spm_id_from=333.999.0.0&jump_opus=1
如何理解线程不安全?
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。线程不安全的本质什么?
- 一个角度:保障 可见性,原子性和有序性。
- 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。Java提供了
volatile
关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。(另外,通过synchronized
和Lock
也能够保证可见性,能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。) - 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized
和Lock
来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。 - 有序性:即程序执行的顺序按照代码的先后顺序执行。
通过volatile
关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。
Java有什么锁?
- 乐观锁
- 最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗(自旋)
- CAS
- 悲观锁
- 最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
- synchronized,reentrantlock
- 锁的选型?
- synchronized:可以用于实现对象级别的同步。它可以修饰方法或代码块;
优点:简单易用,适合用于对临界资源进行简单的同步。适用于简单的同步需求,如小规模的并发控制,或者不需要可中断锁、公平锁等高级功能的场景。
缺点:粒度较粗,性能相对较低。 - ReentrantLock:可重入锁,提供更加灵活的锁机制,支持可中断锁、超时锁和公平锁等。手动控制锁的获取和释放。
优点:灵活性高,支持多种锁特性。适用于复杂的并发控制场景,特别是需要利用条件变量、公平锁、可中断锁等高级功能时。
缺点:相比 synchronized,使用方式稍复杂。 - 对于低竞争、并发性能要求高的场景,可以考虑使用自旋锁或者乐观锁;对于读多写少的场景,可以考虑使用 ReadWriteLock;对于复杂的同步需求,可以考虑使用 ReentrantLock。
- synchronized:可以用于实现对象级别的同步。它可以修饰方法或代码块;
- 乐观锁
synchronized(同步锁)
- Synchronized采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个锁时就会阻塞住;只能实现为非公平锁;而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
1
2
3
4
5
6
7
8
9
10
11public class SynchronizedExample {
public synchronized void syncMethod() {
// 同步方法体
}
public void syncBlock() {
synchronized (this) {
// 同步代码块
}
}
} - 底层实现:synchronized是java提供的原子性内置锁,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
- 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。
- 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
- monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor;其内部有三个属性,分别是owner、entrylist、waitset(事实上有这两个队列)
- owner是关联的获得锁的线程,并且只能关联一个线程;
- entrylist关联的是处于阻塞状态的线程,在owner为空后线程竞争锁;
- waitset关联的是处于Waiting状态的线程
- Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
- 重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。一旦锁发生了竞争,就会升级为重量级锁。
- 轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
- 偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时会有一个CAS操作,之后该线程再获取锁只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
- 完整执行流程:
??先考虑偏向锁,如果当前线程ID与markword存储的不相等,则CAS尝试更换线程ID,CAS成功就获取得到锁了;CAS失败则升级为轻量级锁,实际上也是通过CAS来抢占锁资源(只不过多了拷贝Mark Word到Lock Record的过程),抢占成功到锁就归属给该线程了,但自旋失败一定次数后(竞争激烈??)升级重量级锁;重量级锁通过monitor对象中的队列存储线程,但线程进入队列前,还是会先尝试获取得到锁,如果能获取不到才进入线程等待队列中
- Synchronized采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个锁时就会阻塞住;只能实现为非公平锁;而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
具体来说,Synchronized 锁住的是什么?
- 当
Synchronized
关键字应用在实例方法上时,它锁住的是调用该方法的对象实例,也就是该实例的对象锁。不同实例对象之间的方法调用不会互斥,只有同一个实例对象的方法调用才会互斥,即同一个对象实例的多个线程之间会竞争对象锁。 - 当
Synchronized
关键字应用在静态方法上时,它锁住的是该类的 Class 对象,也就是类的静态锁。这意味着不同实例对象之间的静态方法调用也会互斥,同一个类的不同实例对象的静态方法调用也会互斥,因为它们共享同一个类的 Class 对象。 - 当
Synchronized
关键字应用在代码块中时,它锁住的是指定的对象实例或者类的 Class 对象。例如:这里的1
2
3synchronized (obj) {
// synchronized code block
}obj
可以是任意一个对象实例,如果多个线程使用相同的obj
对象进行同步,那么它们之间就会互斥执行代码块内的代码,因为它们共享同一个对象锁。如果将synchronized
关键字应用在static
代码块中,那么它将锁住类的 Class 对象,同样会导致同一个类的不同实例对象之间互斥执行。
- 当
JMM(Java内存模型)
- 定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存(同时分为共享资源,所以没加锁,具体实现为CAS???)
CAS(Compare And Swap(比较再交换))
- 它体现的一种乐观锁的思想在无锁状态下保证线程操作数据的原子性。
- CAS使用到的地方很多:AQS框架、AtomicXXX类
- 在操作共享变量的时候使用的自旋锁,效率上更高一些(但是反复自旋会导致效率下降)
- CAS的底层是调用的???Unsafe类中的方法,都是操作系统提供的,其他语言实现
- CAS 存在的问题和局限性:
- ABA 问题: ABA 问题是 CAS 中常见的问题之一。当一个线程读取数据 A,然后另一个线程将 A 改变为 B,再改变为 A,此时第一个线程使用 CAS 进行比较并交换时,会发现值仍然为 A,认为没有被修改过,导致可能出现意外结果。
为了解决 ABA 问题,可以使用版本号或者标记来辅助 CAS 操作,确保在比较并交换时,不仅比较值是否相同,还需要比较版本号或者标记是否相同,以此来增强 CAS 的正确性。 - 循环时间长开销大: CAS 是一个自旋操作,当竞争激烈或者线程长时间无法获取到锁时,会导致自旋时间长,消耗大量的 CPU 资源,降低系统性能。
- 只能保证一个共享变量的原子操作: CAS 只能针对一个共享变量进行原子操作,对于多个共享变量的复合操作,需要额外的手段来保证原子性。
- ABA 问题: ABA 问题是 CAS 中常见的问题之一。当一个线程读取数据 A,然后另一个线程将 A 改变为 B,再改变为 A,此时第一个线程使用 CAS 进行比较并交换时,会发现值仍然为 A,认为没有被修改过,导致可能出现意外结果。
volatile
- 保证线程间的可见性
用 volatie 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见 - 禁止进行指令重排序
指令重排:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障阻止其他读写操作越过屏障,从而达到阻止重排序的效果
- 保证线程间的可见性
什么是AQS?
- 多线程中的队列同步器。一种锁机制,它是做为一个基础框架使用的像ReentrantLock、Semaphore都是基于AQS实现的
- AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
- 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
- AQS可以实现为公平锁和非公平锁。
Reentrantlock(可重入锁)
- ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,(同一个线程)再次调用 lock,是不会再阻塞;
支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁1
2
3
4
5
6
7
8
9
10
11
12public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock(); // 默认非公平锁,true 启用公平锁
public void lockMethod() {
lock.lock();
try {
// 受保护的代码区
} finally {
lock.unlock();
}
}
} - 底层实现:ReentrantLock主要利用CAS+AQS队列来实现
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部当
- exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
- 完整过程:
- ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,(同一个线程)再次调用 lock,是不会再阻塞;
synchronized 和 ReentrantLock 的区别? https://www.bilibili.com/opus/788136901432311825?spm_id_from=333.999.0.0
- 实现方式:
synchronized
是 Java 语言级别的关键字,用于实现线程同步。可以修饰代码块或方法,自动获取和释放锁。
ReentrantLock
是 Java API 提供的锁实现,需要显式地使用lock()
和unlock()
获取锁和释放锁。 - 锁粒度:
synchronized
的锁粒度比较粗,它可以对整个方法或代码块进行同步,不能灵活控制锁的粒度。
ReentrantLock
的锁粒度比较细,可以根据需要在代码中灵活地获取和释放锁,可以实现更细粒度的同步。 - 功能层面:
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量;Lock 有适合不同场景的实现,如 ReentrantLock,ReentrantReadWriteLock(读写锁) - 性能:
在低竞争、线程数量不多的情况下,synchronized
的性能可能更好,因为它是 JVM 内置的机制,无需额外的资源消耗。
在高竞争、线程数量较多或者需要更细粒度控制的情况下,ReentrantLock
可以提供更好的灵活性和性能。
- 实现方式:
synchronized 和 violated 的区别?
- synchronized 是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile 本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改某个变量的新值对其他线程是立即可见的;
2、禁止进行指令重排序。 - 1.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- 2.volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性
- 3.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- 4.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
公平锁和非公平锁
- 公平锁:是指多个线程按照请求锁的顺序来获取锁,遵循先来先得的原则。如果锁已被其他线程占用,新来的线程就会进入等待队列。只有当队列中的前面所有线程都获取到锁并释放后,该线程才能获得锁。这种机制虽然保证了锁分配的绝对公平性,但会导致更大的线程切换开销和降低整体吞吐量,频繁的线程调度会消耗大量的系统资源。
- 优点:
1)确保无饥饿发生,每个线程最终都可以获取到锁。
2)适用于事务处理等对执行顺序敏感的场景。 - 缺点:
1)效率较低,因为要保证按照请求顺序获得锁,可能导致线程切换和等待的增加。
2)可能会造成锁的平均获取时间延长。
- 优点:
- 非公平锁:是指多个线程获取锁的顺序并不是按照请求锁的顺序,允许”插队”。新请求的锁可能会直接获得锁,即使有其他线程正在等待。这种方式不保证等待的线程能够按照请求顺序获得锁,但是在多数情况下能够减少唤醒和阻塞的次数,从而提高系统的吞吐量。
- 优点:
1)效率更高,因为减少了队列的调度,可以更快地获得锁,提高了程序的响应速度和吞吐量。
2)在非竞争或低竞争的环境下,性能优于公平锁。 - 缺点:
1)可能会导致线程饥饿,即某些线程可能会很长时间获取不到锁。
2)在高度竞争的环境下,线程可能会不断地尝试获取锁,从而增加CPU的负担。
- 优点:
- 如果让你实现的话,你怎么实现公平和非公平锁?
- 公平锁:可以把竞争的线程放在一个先进先出的队列上。只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁就好了
- 非公平锁:可能会经历以下流程、、
- 直接尝试获取锁: 如果锁当前没有被其他线程持有,该线程会直接获取到锁并执行同步代码。
- 自旋等待:如果锁已经被其他线程持有,线程可能会进行一定次数的自旋等待(反复尝试获取锁),而不立即进入等待队列,这样可以减少线程切换的开销。
- 后端阻塞: 如果自旋等待仍然没有获取到锁,可能会进入后端阻塞状态,此时线程会加入等待队列中,并阻塞等待锁的释放。
- ReentrantLock是如何实现锁公平和非公平性的?
通过维护一个线程队列,按照线程的请求顺序来获取锁。在创建 ReentrantLock 对象时传入了true
参数,则这个锁是公平的;传入了false
参数是非公平锁(默认)。 - synchronized可以实现公平锁和非公平锁吗?
默认非公平锁,无法直接指定为公平锁。在 synchronized 中,线程在尝试获取锁时会直接去竞争锁,而不考虑其他等待线程的顺序。
- 公平锁:是指多个线程按照请求锁的顺序来获取锁,遵循先来先得的原则。如果锁已被其他线程占用,新来的线程就会进入等待队列。只有当队列中的前面所有线程都获取到锁并释放后,该线程才能获得锁。这种机制虽然保证了锁分配的绝对公平性,但会导致更大的线程切换开销和降低整体吞吐量,频繁的线程调度会消耗大量的系统资源。
ConcurrentHashMap、、
- 底层数据结构
JDK1.7底层采用分段的数组+链表实现
JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树 - 加锁的方式
JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
JDK1.8采用CAS添加新节点,(如果已存在节点)采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
- 底层数据结构
ThreadLocal的底层原理
- Threadlocal是java中所提供的线程本地存储机制,可以利用该机制将【资源对象】数据存在某个线程内部,该线程可以在任意时刻、任意方法中获取领存的数据
- Threadlocal底层通过ThreadlocaMap来实现的,每个Thread对象(线程)中都存在ThreadlocalMap,保存多个Threadlocal对象
a)调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
b)调用 get 方法,就是以 ThreadLocal自己作为 key,到当前线程中查找关联的资源值
c)调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值 - ThreadLocal内存泄漏问题
ThreadLocalMap 中的 key是弱引用,值为强引用;key会被GC释放内存,关联 value的内存并不会释放。建议主动 remove 释放 key,value - Threadlocal经典的应用场就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之进行传递,线程之间不共享同一个连接)
线程池
- 线程池是一种用于管理和复用线程的机制,它提供了一种执行大量异步任务的方式,并且可以在多个任务之间合理地分配和管理系统资源。
管理一系列线程的资源池,用到了池化技术,像数据库连接池、HTTP 连接池都是池化技术的体现,主要目的是为了提高资源的利用率。 - 为什么需要线程池?
JVM在HotSpot的线程模型下,Java线程会一对一映射为内核线程。这意味着,在Java中每次创建以及回收线程都会去内核创建以及回收,这就有可能导致:创建和销毁线程所花费的时间和资源可能比处理的任务花费的时间和资源要更多。线程池的出现是为了提高线程的复用性以及固定线程的数量!! - 使用线程池的好处
- 降低资源消耗,通过重复利用已创建的线程来降低创建和销毁线程的消耗。
- 提高响应速度,因为当有任务到达时,线程已经被创建完毕,立刻能执行任务。
- 提高线程的可管理性,可以批量管理线程,并且可以监控和调优,根据实际情况修改参数能将线程池效率发挥最大化。使用线程池可以让多个不相关的任务同时执行。
- 线程池两种创建方式
- 通过 ThreadPoolExecutor 构造函数。(推荐)
- 通过 Executor 框架的工具类 Executors 创建。JDK 1.5 之后引入,相比于直接用 Thread 的 Start 方法更好,方便管理,效率更高。包括线程池管理,还提供线程工厂、任务队列和拒绝策略。
- 线程池是一种用于管理和复用线程的机制,它提供了一种执行大量异步任务的方式,并且可以在多个任务之间合理地分配和管理系统资源。
ThreadPoolExecutor 类
- 7?大参数:
corePoolSize(核心线程数量,即使空闲时也不会回收)、maximumPoolSize(最大线程数量)、keepAliveTime(线程空余时间:若当前运行的线程数大于核心线程数,达到空闲时间后就会对线程进行回收)、workQueue(阻塞(任务)队列)、handler(任务拒绝策略:核心线程满,任务队列满,最大线程数满,再有任务就会执行拒绝策略)
threadFactory:线程工厂 - 任务提交的流程:
- 首先会判断运行线程数是否小于corePoolSize,如果小于,则直接创建新的线程执行任务
- 如果大于corePoolSize,判断workQueue阻塞队列是否已满,如果还没满,则将任务放到阻塞队列中
- 如果workQueue阻塞队列已满,则判断当前线程数是否大于maximumPoolSize,如果没有则创建新的线程执行任务(超出核心线程数,达到线程空余时间后回收)
- 如果大于maximumPoolSize,则执行任务拒绝策略(具体就是你自己实现的handler)
- 线程池的拒绝策略默认有以下 4 种:
1、AbortPolicy(中止策略)功能: 直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程。
2、CallerRunsPolicy(调用者运行策略)功能:只要线程池没有关闭,就由提交任务的当前线程处理。
3、DiscardPolicy(丢弃策略)功能:直接静悄悄的丢弃这个任务,不触发任何动作。
4、DiscardOldestPolicy(弃老策略)功能:如果线程池未关闭,就弹出队列头部的元素,然后尝试执行。 - 如何指定线程数?
线程池指定线程数这块,首先要考量自己的业务是什么样的,是cpu密集型的还是io密集型的。
假设运行应用的机器CPU核心数是N,那cpu密集型的可以先给到N+1,io密集型的可以给到2N去试试;
上面这个只是一个常见的经验做法,具体究竟开多少线程,需要压测才能比较准确地定下来;线程不是说越大越好,多线程是为了充分利用CPU的资源,线程过多的上下文切换也会带来系统的开销
- 7?大参数:
ThreadPoolExecutor 的类型
FixedThreadPool:该方法返回一个固定线程数量的线程池。
SingleThreadExecutor: 该方法返回一个只有一个线程的线程池。
CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。
ScheduledThreadPool:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。Java死锁如何避免?
- 造成死锁的几个原因:
- 一个资源每次只能被一个线程使用
- 一个线程在阻塞等待某个资源时,不释放已占有资源
- 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
- 若干线程形成头尾相接的循环等待资源关系
- 这是造成死锁必须要达到的4个条件,如果要避免死锁,只需不满足其中某一个即可,而其中前3个是作为锁要符合的条件,所以要避免死锁就要打破第4个条件,不出现循环等待锁的关系。在开发过程中:
- 要注意加锁顺序,保证每个线程按同样的顺序进行加锁,比如我们可以使用Hash值的大小来确定加锁的先后
- 尽可能缩减加锁的范围,等到操作共享变量的时候才加锁。
- 要注意加锁时限,可以针对所设置一个超时时间
- 要注意死铁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
- 死锁诊断:
- 当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息,查看日志,检查死锁;如果有死锁现象,需要查看具体代码分析后修复
- 可视化工具jconsole、VisualVM也可以检查死锁问题
- 造成死锁的几个原因:
多线程和多进程、、
- 多线程:是指在同一进程内部同时执行多个线程,共享同一份内存空间,可以访问共享变量,线程之间的通信和同步相对比较容易。
优点:线程切换开销小,共享内存,数据共享方便,适合在单机上进行并发编程。
缺点:共享数据容易导致竞态条件和数据安全问题,需要额外的同步机制来保护共享资源,编程和调试复杂度较高。 - 多进程:是指在操作系统中同时执行多个独立的进程,每个进程拥有独立的内存空间,数据隔离性较好,需要通过进程间通信来进行数据传递和共享。
优点:数据隔离性好,一个进程崩溃不会影响其他进程,适合在多机分布式环境下进行并发编程。
缺点:进程切换开销大,需要额外的通信机制来传递数据,资源消耗较大。 - 适用场景:
多线程适合在单机上进行并发编程,例如在计算密集型任务或者I/O密集型任务中可以利用多线程提高系统的性能和响应速度。
多进程适合在分布式环境下进行并发编程,例如在分布式系统中可以通过多进程来实现任务的分布式计算和数据处理。
- 多线程:是指在同一进程内部同时执行多个线程,共享同一份内存空间,可以访问共享变量,线程之间的通信和同步相对比较容易。
Java IO
Java 中 IO 流分为几种?
- 1、按照流的流向分,可以分为输入流和输出流;
- 2、按照操作单元划分,可以划分为字节流和字符流;
- 3、按照流的角色划分为节点流和处理流。
- Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系,都是从如下 4 个抽象类基类中派生出来的。
InputStream / Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream / Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 - 字节流与字符流的区别
以字节为单位输入输出数据,字节流按照8位传输
以字符为单位输入输出数据,字符流按照16位传输
常用io类有那些
File
FileInputSteam,FileOutputStream
BufferInputStream,BufferedOutputSream
PrintWrite
FileReader,FileWriter
BufferReader,BufferedWriter
ObjectInputStream,ObjectOutputSream
在所列的 Java I/O 类中,处理流通常是指对已有的流进行封装,以提供额外的功能或性能改进。以下是处理流的类:处理流有哪些?
- FileInputStream,FileOutputStream:这两个类是字节流,但它们常常与其他处理流一起使用,如
BufferedInputStream
和BufferedOutputStream
,以提供缓冲功能,提高读写性能。1
2FileInputStream fis = new FileInputStream("example.txt");
BufferedInputStream bis = new BufferedInputStream(fis); - BufferedInputStream,BufferedOutputStream:提供了缓冲区功能,可以减少对底层流的直接读写次数,提高效率。
- FileReader,FileWriter:这两个类是字符流,但它们也可以和
BufferedReader
和BufferedWriter
一起使用。1
2FileReader fr = new FileReader("example.txt");
BufferedReader br = new BufferedReader(fr); - BufferedReader,BufferedWriter:提供了缓冲区功能,可以减少对底层字符流的直接读写次数,提高效率。
- ObjectInputStream,ObjectOutputStream:这两个类用于序列化和反序列化对象。它们可以与其他流一起使用,如
BufferedInputStream
和BufferedOutputStream
。处理流通常用于提供一些额外的功能,如缓冲、对象序列化等。其他类(File、PrintWriter)不是处理流,而是用于文件或文本输出的类。1
2FileOutputStream fos = new FileOutputStream("object.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);
- FileInputStream,FileOutputStream:这两个类是字节流,但它们常常与其他处理流一起使用,如
操作系统的IO
- 我们要将内存中的数据写入到磁盘的话,主体可能是一个应用程序,比如一个Java进程(假设网络传来二进制流,一个Java进程可以把它写入到磁盘)。
应用程序是跑在用户空间的,它不存在实质的IO过程,真正的IO是在操作系统执行的。即应用程序的IO操作分为两种动作:IO调用和IO执行。IO调用是由进程(应用程序的运行态)发起,而IO执行是操作系统内核的工作。此时所说的IO是应用程序对操作系统IO功能的一次触发,即IO调用。 - 操作系统负责计算机的资源管理和进程的调度。我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。也就是说,你的应用程序要把数据写入磁盘,只能通过调用操作系统开放出来的API来操作。
- 一个完整的IO过程包括以下几个步骤:
应用程序进程向操作系统发起I0调用请求
操作系统准备数据,把I0外部设备的数据,加载到内核缓冲区
操作系统拷贝数据,即将内核缓冲区的数据,拷贝到用户进程缓冲区 - 阻塞分类:
同步阻塞(blocking-I0)简称BIO
同步非阻塞(non-blocking-10)简称NIO
异步非阻塞(asynchronous-non-blocking-10)简称AI0
- 我们要将内存中的数据写入到磁盘的话,主体可能是一个应用程序,比如一个Java进程(假设网络传来二进制流,一个Java进程可以把它写入到磁盘)。
五大阻塞模型: 阻塞 IO 模型、非阻塞 IO 模型、多路复用 IO 模型、信号驱动 IO 模型、异步 IO 模型
- 阻塞 IO 模型(如socket、Java BIO)
最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。
典型的阻塞 IO 模型的例子为: data = socket.read(); 如果数据没有就绪,就会一直阻塞在 read 方法 - 非阻塞 IO 模型
- 当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。 如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。一个非常严重的问题, 在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。 - 典型的非阻塞 IO 模型一般如下:
1
2
3
4
5
6while(true){
data = socket.read();
if(data!= error){
//处理数据break;
}
}
- 当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。 如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
- 多路复用 I/O 模型
- 多路复用 IO 模型是目前使用得比较多的模型。 Java NIO 实际上就是多路复用 IO。
- IO复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),它们可以同时监控多个 fd(文件标识符)的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom 系统调用。
- 在多路复用 IO模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用IO 资源,所以它大大减少了资源占用。
IO 为何比非阻塞 IO 模型的效率高?是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。 - 不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型, 一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
1
2
3
4
5fds = wait_files({fd1, fd2, .. fdn}, ..); // 内核轮询多个文件标识符,读写多个文件
for (fd : fds) {
read(fd, buf);
do_something(buf);
}
- 信号驱动 IO 模型
当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。 - 异步 IO 模型
异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的, 只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了。
也就说在异步 IO 模型中, IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用 IO 函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;而在异步 IO 模型中,收到信号表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。 注意,异步 IO 是需要操作系统的底层支持,在 Java 7 中,提供了 Asynchronous IO。
- 阻塞 IO 模型(如socket、Java BIO)
I/O复用模型的select、poll、epoll函数
- select:Unix/Linux 系统中最早的 I/O 复用函数之一,它通过一个文件描述符集合来监视多个文件描述符的状态变化。
- 使用
fd_set
结构来表示文件描述符集合,通过 FD_ZERO、FD_SET、FD_CLR 等宏来对文件描述符集合进行操作。 select
函数会阻塞,直到集合中任意一个文件描述符准备就绪或超时。轮询、、遍历文件描述符- 缺点是支持的文件描述符数量有限(一般为 1024),且效率较低,因为每次调用都需要将整个文件描述符集合从用户空间拷贝到内核空间。
- 使用
- poll:对
select
函数的改进,同样是通过一个文件描述符集合来监视多个文件描述符的状态变化。- 使用
struct pollfd
结构来表示文件描述符集合,通过poll
函数进行监视。 poll
函数会阻塞,直到集合中任意一个文件描述符准备就绪或超时。- 缺点是同样存在文件描述符数量有限的问题,且效率相对于
select
提升不大。
- 使用
- epoll:Linux 特有的 I/O 复用函数,是对 select 和 poll 的改进,效率更高。
- 使用
epoll_create
创建一个 epoll 实例,并使用epoll_ctl
添加、修改、删除要监听的文件描述符。 epoll_wait
函数用于等待文件描述符集合中任意一个文件描述符准备就绪或超时。事件驱动、、监听事件回调机制:某个fd就绪后,内核回调激活- epoll 支持水平触发和边缘触发两种模式,边缘触发模式只在状态发生变化时通知程序。
- epoll 的优势在于它可以监视大量的文件描述符(最大数量受系统限制),并且不需要每次调用都将文件描述符集合拷贝到内核空间,因此效率更高。
- 使用
- select:Unix/Linux 系统中最早的 I/O 复用函数之一,它通过一个文件描述符集合来监视多个文件描述符的状态变化。
Java 的 NIO?
- https://www.bilibili.com/read/cv22750549/?spm_id_from=333.999.0.0&jump_opus=1
Java NIO 是JDK 1.4 开始有的,其目的是为了提高速度。传统IO是一次一个字节地处理数据,NIO是以块(缓冲区)的形式处理数据,所以NIO的效率要比IO高很多。最主要的是,NIO可以实现非阻塞,而传统IO只能是阻塞的。IO的实际场景是文件IO和网络IO,NIO在网络IO场景下提升就尤其明显了。
在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。 - ???NIO 的缓冲区
Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据, 需要先将它缓存到一个缓冲区。 NIO 的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。 - ???NIO 的非阻塞
IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
- https://www.bilibili.com/read/cv22750549/?spm_id_from=333.999.0.0&jump_opus=1
Channel
首先说一下 Channel,国内大多翻译成“通道”。 Channel 和 IO 中的 Stream(流)是差不多一个等级的。
只不过 Stream 是单向的,譬如:InputStream, OutputStream, 而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO 中的 Channel 的主要实现有:1、FileChannel 2、DatagramChannel 3、SocketChannel 4、ServerSocketChannel,分别可以对应文件 IO、 UDP 和 TCP(Server 和 Client)。Buffer
Buffer,缓冲区,实际上是一个容器,是一个连续数组。 Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
下图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入 Buffer 中,然后将Buffer 中的内容写入通道。服务端这边接收数据必须通过 Channel 将数据读入到 Buffer 中,然后再从 Buffer 中取出数据来处理。在 NIO 中, Buffer 是一个顶层父类,它是一个抽象类,常用的 Buffer 的子类有:ByteBuffer、 IntBuffer、 CharBuffer、 LongBuffer、DoubleBuffer、 FloatBuffer、ShortBuffer???Selector
Selector 类是 NIO 的核心类, Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。???介绍一下自己对 Netty 的认识。
第一:Netty 是一个 基于 NIO 模型的高性能网络通信框架,其实可以认为它是对 NIO 网络模型的封装,提供了简单易用的 API,我们可以利用这些封装好的API 快速开发自己的网络程序。
第二:Netty 在 NIO 的基础上做了很多优化,比如零拷贝机制、高性能无锁队列、内存池等,因此性能会比 NIO 更高。
第三:Netty 可以支持多种通信协议,如 Http、WebSocket 等,并且针对数据通信的拆包黏包问题,Netty 内置了拆包策略。如何读取一个不知道类型的文件A数据到文件B中?
指定输入文件 input.txt 和输出文件 output.txt 的路径,然后使用 BufferedReader 和 BufferedWriter 来读写文件。
在 while 循环中,使用 readLine() 逐行读取文件 A 中的数据,然后将每一行数据写入到文件 B 中。通过异常处理来捕获可能出现的 IO 异常。
以上是针对文本文件的读取和写入,如果要处理的是二进制文件或者特定格式的文件,需要使用对应的输入输出流和处理方式。例如,可以使用 FileInputStream 和 FileOutputStream 来处理二进制文件,使用第三方库如 Apache POI 来处理 Excel 文件等。QPS、TPS、RT以及吞吐量
- QPS : 每秒查询数,表示系统在一秒内处理的查询或请求的数量。对于数据库、Web服务器等服务,QPS表示每秒处理的查询请求次数。
- TPS: 每秒事务数,表示系统在一秒内处理的事务的数量。在分布式系统和事务性系统中,TPS是一个更为常见的指标,用于表示系统的事务处理能力。
- KBS: 衡量系统每秒传输的数据量,通常用于评估网络或存储系统的性能。表示每秒传输的数据量为千字节。
- RT : 响应时间,表示系统处理一个请求所花费的时间。通常以毫秒(ms)为单位。较低的响应时间通常表示系统性能较好,对用户体验更为友好。
- 吞吐量: 表示在单位时间内处理的数据量或请求总数。在性能测试中,吞吐量是一个综合指标,它同时考虑了系统的处理速度和响应时间。通常以每秒处理的请求数、事务数、或数据量为单位。
大数据问题
大数据问题1:如何读取写入100万条数据?
数据保存在Excel的20个sheet页中,即每一个sheet中有5万条数据。实现在3秒内导入100万条数据需要高效地处理数据导入和并发操作.
通过线程池创建了20个线程,每个线程负责导入一个 sheet 中的数据;其中每个sheet又创建5个线程,分别处理1万条数据,将这1万条数据批处理地插入到数据库中。
https://www.bilibili.com/list/watchlater?oid=1001335801&bvid=BV1gx4y1k7ks&spm_id_from=333.788.top_right_bar_window_view_later.content.click大数据问题2:如何从十万条数据中挑选最符合要求的十条?
假设数据是包含对象的列表,我们可以使用 Java 8 的流操作和排序来解决这个问题。1
2
3
4
5
6
7public static List<DataObject> filterData(List<DataObject> dataList, int count) {
return dataList.stream()
.filter(/* Your filtering condition */) // 根据需要添加自定义的条件
.sorted(Comparator.comparing(/* Your sorting criteria */))
.limit(count) // 排序和限制用于选择最符合条件的数据
.collect(Collectors.toList());
}批处理,多线程处理,流处理。
- 批处理(Batch Processing):
- 批处理是一种将一组任务按顺序批量处理的方式,通常是将多个作业或任务一次性提交给计算机系统执行,而不需要用户干预。
- 批处理通常是指在一个独立的进程中依次执行一系列命令或操作,可以是顺序执行、并行执行、循环执行等,但并不一定是多线程处理。每个命令或操作都会等待上一个任务执行完成后才会执行,直到所有任务完成。
- 在计算机领域中,批处理可以用于自动化执行一系列重复性的任务,比如批量处理文件、数据批量导入导出、批量备份等。
- 多线程处理(Multithreading):
- 多线程处理是指在一个程序中同时执行多个线程,每个线程可以独立执行不同的任务,共享相同的内存空间。
- 多线程处理可以提高程序的并发性和效率,充分利用计算机的多核处理器和资源,可以同时处理多个任务,提高程序的响应速度和并发能力。
- 多线程处理通常用于需要同时执行多个任务或需要实现并发性的应用程序,比如网络服务器、图形界面应用程序、多媒体处理等。
- 流处理(Stream Processing):
- 流处理是一种连续不断地处理数据流的方式,通常是从输入端读取数据,经过一系列的处理操作,然后输出到输出端,而不需要存储整个数据集。
- 流处理可以实现实时处理和即时响应,能够处理大规模的数据流,适用于实时计算、实时分析、实时监控等场景。
- 流处理通常用于处理实时数据流,比如实时日志处理、实时事件处理、实时数据分析等。常见的流处理框架包括Apache Kafka、Apache Flink等。
- 批处理(Batch Processing):
如何判断40亿个数中某个数不存在?(布隆过滤器)
- 布隆过滤器是一种数据结构,可以用于快速判断一个元素是否可能存在于一个集合中。它的基本原理是使用多个哈希函数将元素映射到一个位数组中,当检查一个元素是否存在时,只需检查对应位数组上的位是否都为1。具体步骤如下:
- 初始化一个位数组,长度要足够大,可以容纳 40 亿个数的哈希结果。
- 使用多个哈希函数对需要判断的数进行哈希,得到多个哈希结果。
- 将这些哈希结果对应的位数组位置设为1。
- 当需要判断某个数是否存在时,使用同样的多个哈希函数对该数进行哈希,并检查对应的位数组位置是否都为1。
- 如果都为1,则说明该数可能存在于集合中;如果有任何一位为0,则说明该数一定不存在于集合中。
需要注意的是,布隆过滤器存在一定的误判率,即有可能判断某个数不存在于集合中,但实际上它确实存在。因此,在使用布隆过滤器时,需要权衡误判率和内存占用等因素,选择合适的参数配置。
- 布隆过滤器是一种数据结构,可以用于快速判断一个元素是否可能存在于一个集合中。它的基本原理是使用多个哈希函数将元素映射到一个位数组中,当检查一个元素是否存在时,只需检查对应位数组上的位是否都为1。具体步骤如下:
如何在1万个数里面找到最大的100个?(堆排序)
- 维护一个大小为100的小顶堆(堆顶元素最小)。
- 遍历1万个数,对于每个数,如果大于堆顶元素,则将堆顶元素替换为当前数,并进行堆调整。
- 遍历结束后,堆中的100个元素就是最大的100个数。
足球场上5万名观众对他们的年龄排序,怎么最快?(快速排序算法)
- 选择一个基准元素,将小于基准的元素放在基准的左边,大于基准的元素放在基准的右边。
- 递归地对基准左右两边的子数组进行排序,直到数组有序。
- 另外,如果需要最快的排序算法,可以考虑使用基数排序或计数排序。这两种排序算法在特定条件下可以达到线性时间复杂度,但需要满足一定的条件(例如数据范围不大且为整数)。
有一批用户(百万数量级)的杂乱行为数据(千万数量级)每一条都要重新洗数,且洗数过程中同一用户的前后行为有时序性依赖关系(前一个行为的洗数结果是下一个行为进行洗数的必要条件),如果是你会怎么设计洗数流程?要求正确、高效、抗风险、、
- 分布式任务调度: 使用分布式任务调度系统(如Kafka、Spring Cloud Data Flow等)来调度洗数任务。这样可以方便地将任务分发到多个节点上并行执行,提高整体处理效率。
- 数据分片处理: 将用户的杂乱行为数据进行分片,每个分片包含一部分用户的数据。然后将每个分片分配给不同的洗数任务处理。这样可以减小单个任务的压力,提高并行处理效率。
- 洗数任务设计: 每个洗数任务负责处理一个用户的行为数据,并保证前后行为的时序性依赖关系。可以采用状态机等方法来实现对时序性依赖关系的处理。任务之间可以通过消息队列等方式进行通信,保证数据的正确性和一致性。
- 结果合并: 每个洗数任务处理完数据后,将结果写入到目标存储(如数据库、HDFS等)。另外可以设计一个结果合并任务,负责将所有洗数任务的结果合并,形成最终的洗数结果。
- 监控和异常处理: 设计监控系统,实时监控洗数任务的运行情况和数据处理进度,及时发现和处理异常。对于处理失败的数据,可采用重试、补偿等策略保证数据的完整性和准确性。
- 性能优化: 针对百万数量级的用户和千万数量级的行为数据,需要进行性能优化,包括优化数据读取、处理和写入的性能,以及任务调度和并行处理的性能等方面。
- 容灾和备份: 设计容灾和备份方案,确保系统在遇到故障或者灾难时能够快速恢复。可以采用数据备份、主备切换、数据同步等方式来保证系统的高可用性和可靠性。
Spring
Spring (Spring Framework) 是什么?
Spring 是个java企业级应用的开源开发框架,旨在降低应用程序开发的复杂度。它是轻量级、松散耦合的。它具有分层体系结构,允许用户选择组件(选择需要的模块:Core、AOP、WEB、、)。它可以集成其他框架,如 Structs、Hibernate、EJB 等,所以又称为框架的框架。
框架—–》容器——》生态、、Spring的可扩展性指?
框架本身的设计和架构能够方便地接受和整合新的功能、模块或者扩展点,同时允许开发者通过自定义实现来扩展或修改框架的行为。这一特性使得Spring框架在不断演进的技术环境中能够灵活地适应新的需求和变化。具体而言,Spring框架的可扩展性主要表现在以下几个方面:
1、模块化设计: Spring框架采用模块化的设计,将不同的功能划分为独立的模块。每个模块都有清晰的职责和接口,使得开发者可以根据需要选择性地集成所需的功能,而不必引入整个框架。这样的设计使得Spring更加轻量、可定制。
2、扩展点和接口: Spring框架提供了许多扩展点和接口,允许开发者通过自定义实现来增加新的功能或者修改现有功能。例如,BeanPostProcessor接口可以用于在Bean实例化和初始化的过程中对Bean进行定制化操作。
3、第三方整合: Spring框架支持与许多其他框架和技术的集成,如Hibernate、MyBatis、Quartz等。这种集成性使得开发者可以充分利用其他优秀框架的功能,同时也能够方便地替换或升级这些框架。
4、自定义注解和注解驱动: Spring支持自定义注解,可以通过注解来声明配置信息、定义切面等。注解驱动的开发方式使得开发者能够以更简洁的方式实现特定的功能,同时也为自定义扩展提供了便利。
5、事件驱动机制: Spring框架引入了事件驱动的机制,通过Application Event和ApplicationListener接口,开发者可以实现自定义事件和监听器,从而实现在框架中添加新的业务逻辑或处理逻辑。你们项目中为什么使用Spring框架?Spring框架的好处,特点。
轻量: Spring 是轻量的,基本的版本大约2MB。
控制反转: Spring通过控制反转实现了松散稠合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
面向切面的编程(AOP): Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
容器: Spring 包含并管理应用中对象的生命周期和配置。
MVC框架: Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
事务管理: Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。(编程式事务难)
异常处理: Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate orJDO抛出的) 转化为致的unchecked 异常。使用 Spring 有哪些方式?
- 作为一个成熟的 Spring Web 应用程序:Spring 提供了 Spring MVC 框架,用于构建 Web 层,同时支持通过 Spring Boot 等工具轻松配置和启动整个应用。这种方式下,Spring 负责管理应用程序的各个层次,包括表现层、业务逻辑层和数据访问层。
- 作为第三方 Web 框架,使用 Spring Frameworks 中间层:可以将 Spring 作为第三方框架集成到现有的 Web 应用程序中,而不必完全采用 Spring Web 应用程序的方式。在这种情况下,可能只使用 Spring 的某些模块,例如 Spring IOC 容器、AOP、事务管理等,以提升现有 Web 应用程序的功能和效率。
- 用于远程使用:这可能指的是将 Spring 作为远程服务的一部分,例如通过 Spring 的远程调用支持(如 Spring Remoting 或 Spring Cloud)在分布式系统中进行远程通信。在这种场景下,Spring 的特性可以用于实现远程服务的注册、调用和管理。
- 作为企业级 Java Bean,它可以包装现有的 POJO(Plain Old Java Objects):通过 Spring 的依赖注入和其他特性,可以更方便地管理和组装这些对象,使其更易于测试、扩展和维护。这种方式下,Spring 赋予了这些普通对象更多的企业级特性。
Spring 配置文件?(”Spring bean配置文件”是其中的一个子集)
- 通常,当说 “Spring 配置文件” 时,是指包含了整个 Spring 应用程序配置信息的 XML 文件。Spring 配置文件通常是一个 XML 文件,用于配置 Spring 容器的行为,包括定义和配置 Spring 中的各种组件(Bean)。这个文件包含了 Spring Bean 的定义、依赖关系、AOP 配置、事务管理等信息。
- 在 Spring 的 XML 配置文件中,可以使用
<bean>
元素来定义和配置 Spring Bean 类信息,这些类可以是普通的 Java 类、业务逻辑类、数据访问类等。每个<bean>
元素包含了对应类的一些信息,例如类名、ID、作用域、属性值等。这样,Spring 容器在启动时会根据配置文件中的信息来实例化和管理这些类,使得它们成为 Spring 管理的 Bean。1
2
3
4
5
6
7
8
9
10
11
12<!-- Spring 配置文件 applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 定义一个名为 "myBean" 的 Bean,它是 com.example.MyBean 类的实例 -->
<bean id="myBean" class="com.example.MyBean">
<!-- 设置属性值 -->
<property name="propertyName" value="propertyValue"/>
</bean>
<!-- 其他 Bean 的定义 -->
</beans>
Spring 程序启动步骤。
- 运行一个 Spring 程序通常需要进行以下几步配置:
- 添加 Spring 相关的依赖:在项目的构建工具(如 Maven、Gradle)中,添加 Spring 相关的依赖,包括核心容器、AOP 模块、数据访问模块、Web 模块等,具体依赖根据项目需求而定。
- 创建 Spring 配置文件:创建一个 XML 文件用于配置 Spring 容器。这个配置文件通常包含了应用程序的配置信息,包括定义和配置 Spring Bean,设置数据库连接,配置事务管理等。
1
2
3
4
5<!-- 示例:Spring 配置文件 applicationContext.xml -->
<beans xmlns="" >
<!-- 配置 Spring Bean -->
<bean id="myBean" class="com.example.MyBean"> </bean>
</beans> - 初始化 Spring 容器:在应用程序的启动阶段,通过加载 Spring 配置文件,初始化 Spring 容器。可以使用
ClassPathXmlApplicationContext
或FileSystemXmlApplicationContext
等容器实现类。1
2// 示例:初始化 Spring 容器
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); - 获取 Spring Bean:通过 Spring 容器获取已配置的 Bean。通过容器的
getBean()
方法获取 Bean 的实例。1
2// 示例:获取 Spring Bean
MyBean myBean = (MyBean) context.getBean("myBean"); - ???运行应用程序:也可以通过注解、AOP 等方式利用 Spring 提供的功能。
1
2// 示例:运行应用程序
myBean.doSomething();
- 使用 Spring Boot 启动类来运行程序的一般步骤:(对Spring的配置进行了简化,,)
- 添加 Spring Boot 依赖:在项目的构建工具中(如 Maven、Gradle),添加 Spring Boot 相关的依赖,包括
spring-boot-starter
或其他特定模块,根据项目需求选择。 - 创建 Spring Boot 启动类:创建一个 Java 类,通常命名为
Application
或其他类似的名字,作为 Spring Boot 应用程序的启动类。@SpringBootApplication
是一个组合注解,包含了@SpringBootConfiguration
、@EnableAutoConfiguration
和@ComponentScan
。这个注解表明这是一个 Spring Boot 应用程序的入口类。1
2
3
4
5
6
7// 示例:Spring Boot 启动类
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
} - 编写业务逻辑: 在应用程序中编写业务逻辑代码,可以创建其他的 Spring Bean,通过依赖注入的方式使用这些 Bean。
- 运行应用程序: 通过运行 Spring Boot 启动类的
main
方法启动应用程序。这个方法会启动 Spring Boot 内嵌的 Tomcat 服务器,并自动扫描和配置 Spring Bean。1
2
3
4// 示例:运行 Spring Boot 应用程序
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
- 添加 Spring Boot 依赖:在项目的构建工具中(如 Maven、Gradle),添加 Spring Boot 相关的依赖,包括
- 运行一个 Spring 程序通常需要进行以下几步配置:
“三层架构”:表现层 + 业务层 + 数据访问层
- 三层架构(Presentation Layer、Business Logic Layer、Data Access Layer)是一种常见的软件架构模式,用于将一个软件系统划分为三个主要的逻辑层,以提高系统的可维护性、可扩展性和可重用性。Spring 框架提供了广泛的支持,使得开发者能够更轻松地实现和管理三层架构。
- 表现层(Presentation Layer):
定义: 主要负责用户界面和用户交互,通常包括 Web 页面、UI 组件等。
Spring 支持: Spring 提供了 Spring MVC 模块,用于实现 Web 应用的表现层。Spring MVC 提供了强大的控制器(Controller)机制,支持基于注解的映射、视图解析、数据绑定等功能,使得开发者能够轻松地构建和管理 Web 层。 - 业务层(Business Logic Layer):
定义: 包含了应用程序的业务逻辑,处理用户请求、调用数据访问层进行数据操作,并进行业务规则的处理。
Spring 支持: Spring 提供了 IoC(Inversion of Control)容器和 AOP(Aspect-Oriented Programming)功能,这两者共同构成了 Spring 的核心。开发者可以使用 Spring IoC 容器来管理业务层的对象,而 Spring AOP 可以用于处理横切关注点,如事务管理、日志记录等。 - 数据访问层(Data Access Layer):
定义: 主要用于与数据存储交互,进行数据库访问、数据持久化等操作。
Spring 支持: Spring 提供了对数据访问的支持,其中最重要的是 Spring 的 JDBC 模块和 Spring 的 ORM 模块。Spring JDBC 简化了 JDBC 操作,而 Spring ORM 支持集成多种 ORM 框架,如 Hibernate、MyBatis 等,使得数据访问层的开发更加便捷。
Service层 为什么用的是接口?为什么不直接使用实现类??
- Service 层负责处理业务逻辑、调用数据访问层(DAO,Data Access Object)并与控制器层进行交互。使用接口而不是直接使用实现类呢,这主要有以下几个原因:
- 解耦和可扩展性:使用接口将 Service 层与其实现类解耦。通过面向接口编程,控制器(或其他类)可以只依赖于接口而不是具体的实现类。这样使得代码更加灵活,能够轻松切换不同的实现类或者模拟测试用的虚拟实现。
- 单一职责原则:接口定义了 Service 层的契约和行为,实现类负责具体的逻辑实现。这符合单一职责原则,即一个类应该只负责一项职责。
- 测试和模拟:??接口的使用使得单元测试更加容易。在测试时,可以使用模拟实现来替代真正的实现类,从而更好地进行单元测试。通过模拟,可以控制和验证不同的行为,而无需依赖于底层实现细节。
- 依赖注入:??Spring 容器能够通过依赖注入将接口的实现类注入到需要的地方,而不需要直接关注具体的实现细节。
- 切换一个接口下的不同的实现类有什么意义?为什么不直接使用不同的实现类?
- 灵活性和可维护性:使用接口定义规范可以提高代码的灵活性。通过面向接口编程,可以将调用方与具体实现类解耦,使得代码更易于维护和修改。如果后续需要替换实现类或引入新的实现,只需要修改实现类的绑定,而不需要修改调用方的代码。
- 解耦和依赖注入:接口的使用支持依赖注入,使得系统更易于管理和测试。依赖注入能够减少类之间的耦合度,提高了代码的可测试性,有利于单元测试和模拟测试。
- 扩展性和适应性:使用接口和不同的实现类使得系统更具扩展性。根据不同的需求和场景,可以轻松地切换实现类,使得系统更具适应性和灵活性。
- 遵循设计原则:使用接口遵循了面向对象编程的设计原则,如开闭原则(对扩展开放,对修改关闭)、单一职责原则等。这种设计模式使得代码更清晰、更易于理解和维护。
- Service 层负责处理业务逻辑、调用数据访问层(DAO,Data Access Object)并与控制器层进行交互。使用接口而不是直接使用实现类呢,这主要有以下几个原因:
@Component, @Controller, @Repository,@Service 有何区别?
@Component :这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。
@Controller :这将一个类标记为 Spring Web MVC 控制器。标有它的Bean 会自动导入到 IoC 容器中。
@Service:此注解是组件注解的特化。不对 @Component 提供任何其他行为。在服务层类中使用@Service 而不是@Component,因为它以更好的方式指定了意图。
@Repository :这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器,并使未经检查的异常有资格转换为 Spring DataAccessException。@Autowired 为什么不推荐使用了,而是使用构造函数?
@Autowired
仍然是 Spring 框架中用于进行依赖注入的一种方式,它可以用于字段、构造函数和方法上。然而,一些开发者倾向于使用构造函数注入,而不是字段注入或方法注入,这是因为构造函数注入有一些优势:- 显式性和明确性: 构造函数注入更加显式和明确。通过在构造函数参数上使用
@Autowired
注解,你可以清晰地看到依赖关系,而不用深入查看类的其他部分。 - 不可变性:通过构造函数注入,你可以将依赖关系声明为不可变的,即一旦对象被创建,它的依赖关系就不能再被修改。这有助于确保对象在使用过程中保持一致性。
- 避免循环依赖问题:使用构造函数注入可以有效地避免循环依赖的问题。如果两个类相互依赖,通过构造函数注入,它们将无法创建循环依赖。
- 测试方便:构造函数注入使得在单元测试中更容易进行模拟和注入测试数据。
@Autowired 与 @Resource的区别。
- 相同点:都是做bean的注入时使用,都可以写在字段和setter方法上。两者如果都写在字段上,就不需再写setter。(注:最好是放在setter方法上,因为这样更符合面向对象的思想,通过set、get去操作属性,而不是直接去操作属性。)
- @Autowired 可以更准确地控制应该在何处以及如何进行自动装配。此注解用于在 setter 方法,构造函数,具有任意名称或多个参数的属性或方法上自动装配bean。默认情况下,它是类型驱动的注入。
- @Required 应用于 bean 属性 setter 方法。此注解仅指示必须在配置时使用bean 定义中的显式属性值或使用自动装配填充受影响的 bean属性。如果尚未填充受影响的 bean 属性,则容器将抛出 eanInitializationException。
- 不同点
- 源头不同:
@Autowired
是 Spring 框架自带的注解,属于 Spring 核心;@Resource
是 Java EE提供的注解,不仅被 Spring 支持,也可以在其他 Java EE 容器中使用。
- 注入类型不同:
@Autowired
: 主要通过 byType 的方式进行注入,即按照属性的数据类型从 Spring 容器中匹配并注入。如果存在多个匹配的 Bean,则可以通过@Qualifier
进行进一步指定。1
2
3
private String nameDao;@Resource
: 主要通过 byName 的方式进行注入,即按照属性的名称从 Spring 容器中匹配并注入。也可以通过name
属性指定要注入的 Bean 名称。作用相当于@Autowired,只不过@Autowired按照byType自动注入。@Resource装配顺序:1
2
3
4
public void setName(String name){
this.name=name;
}
①如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则掀出异常。
②如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
③如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
④如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。
- 可选性:
@Autowired
:默认必须注入的,如果找不到匹配的 Bean,会抛出异常。但可以通过设置required = false
来允许 null 值。@Resource
: 默认是必须注入的,不支持设置类似required
的属性。但可以通过@Resource(lookup = "someName")
指定lookup
属性,如果找不到匹配的 Bean,则不会抛出异常。
- 源头不同:
- 相同点:都是做bean的注入时使用,都可以写在字段和setter方法上。两者如果都写在字段上,就不需再写setter。(注:最好是放在setter方法上,因为这样更符合面向对象的思想,通过set、get去操作属性,而不是直接去操作属性。)
Spring 核心模块
Spring主要由以下几个模块组成:
- Core Container
- Spring Core: 核心类库,提供IOC服务;
- Spring Bean:构成用户应用程序主干的对象。Bean 由 Spring IoC 容器管理、实例化、配置、装配和管理。Bean 是基于用户提供给容器的配置元数据创建。
- Spring Context: 提供框架式的Bean访问方式,以及企业级功能 (JNDI、定时任务等);
- Spring AOP: AOP服务,面向切面编程;
- Data Access
- Spring DAO: 对JDBC的抽象,简化了数据访问异常的处理:
- Spring ORM: 对现有的ORM框架的支持;
- Web
- Spring Web: 提供了基本的面向Web的综合特性,例如多方文件上传
- Spring MVC:提供面向Web应用的Model-View-Controller实现。
- Servlet
- Socket
- Test:该层为使用 JUnit 和 TestNG 进行测试提供支持。
- 几个杂项模块: Messaging – 该模块为 STOMP 提供支持。它还支持注解编程模型,该模型用于从 WebSocket 客户端路由和处理 STOMP 消息。Aspects –该模块为与 AspectJ 的集成提供支持。
- Core Container
Spring Bean。一个 Spring Bean 定义 包含什么?
Spring beans 是那些形成 Spring 应用的主干的 java 对象。它们被 Spring IOC容器初始化,装配和管理。这些 beans 通过容器中配置的元数据创建,比如以 XML 文件中 的形式定义。Spring 框架定义的 beans 都是单件beans。在 bean tag 中有个属性”singleton”,如果它被赋为 TRUE,bean 就是单件,否则就是一个prototype bean。默认是 TRUE,所以所有在 Spring 框架中的 beans 缺省都是单件。
一个 Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。Spring 提供了哪些配置方式?一般是怎么定义Bean的?
- 基于 xml 配置
bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。例如:1
2
3<bean id="studentbean" class="org.edureka.firstSpring.StudentBean">
<property name="name" value="Edureka"></property>
</bean> - 基于注解配置
通过在相关的类、方法或字段声明上使用注解,将 bean 配置为组件类本身,而不是使用 XML 来描述 bean 装配。默认情况下,Spring 容器中未打开注解装配。因此,您需要在使用它之前在 Spring 配置文件中启用它。例如:1
2
3
public class MyBean {
}1
2
3
4<beans>
<context:annotation-config/>
<!-- bean definitions go here -->
</beans> - ???基于 Java API 配置
Spring 的 Java 配置是通过使用 @Bean 和 @Configuration 来实现。
1、 @Bean 注解扮演与 元素相同的角色。
2、 @Configuration 类允许通过简单地调用同一个类中的其他 @Bean 方法来定义 bean 间依赖关系。 例如:1
2
3
4
5
6public class StudentConfig {
public StudentBean myStudent() {
return new StudentBean();
}
}
- 基于 xml 配置
依赖注入的方法有几种?
@Autowired
是如何工作的?- 构造器注入:将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。
优点:对象初始化完成后便可获得可使用的对象。
缺点: 当需要注入的对象很多时,构造器参数列表将会很长,不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数,麻烦。1
2
3
4
public MyClass(MyDependency myDependency) {
this.myDependency = myDependency;
} - setter方法注入:IOC Service Provider通过调用成员变量提供的setter函数将被依赖对象注入给依赖类
优点: 灵活,可以选择性地注入需要的对象。用的最多。
缺点: 依赖对象初始化完成后,由于尚未注入被依赖对象,因此还不能使用。1
2
3
4
public void setMyDependency(MyDependency myDependency) {
this.myDependency = myDependency;
} - 接口注入:依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参数就是要注入的对象。
优点: 接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。
缺点:侵入性太强,不建议使用。PS:什么是侵入?如果类A要使用别人提供的一个功能,若为了使用这功能,需要在自己的类中增加额外的代码,这就是侵入性。1
2
3
4
5
6
7
8
9
10
11
12
13
14public interface MyInterface {
void injectDependency(MyDependency myDependency);
}
public class MyClass implements MyInterface {
private MyDependency myDependency;
public void injectDependency(MyDependency myDependency) {
this.myDependency = myDependency;
}
} - 使用建议:用构造器参数实现强制依赖,setter 方法实现可选依赖。
- 使用 @Autowired 注解可以对字段进行依赖注入。具体注入方式取决于被注入的字段的类型,可以是构造器注入、setter 方法注入、接口注入等。
- 构造器注入:将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。
- Spring IOC
Spring IOC 解决的是对象管理和对象依赖的问题。IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系;需要用对象的时候,从工厂里边获取就好了- 「控制反转」指:把原有自己掌控的事交给别人去处理。它更多的是一种思想或者可以理解为设计模式。比如:本来由我们自己new出来的对象,现在交由IOC容器,把对象的控制权交给它方
- 「注入依赖」是「控制反转」的实现方式,对象无需自行创建或者管理它的依赖关系,依赖关系将被「自动注入」到需要它们的对象当中去
- 用 Spring IOC 有什么好处吗?主要在于「将对象集中统一管理」并且「降低耦合度」
如果项目里的对象都是就new下就完事了,没有多个实现类,那没事,不用Spring也没啥问题。但 Spring核心不仅仅IOC啊,除了把对象创建出来,还有一整套的Bean生命周期管理。用Spring IOC 可以方便 单元测试、对象创建复杂、对象依赖复杂、单例等等的,什么都可以交给Spring IOC - 列举 IoC 的一些好处。
它将最小化应用程序中的代码量。
???它将使您的应用程序易于测试,因为它不需要单元测试用例中的任何单例或 JNDI 查找机制。
它以最小的影响和最少的侵入机制促进松耦合。
支持即时的实例化和延迟加载服务。 - spring 中有多少种 IOC 容器?
- BeanFactory - BeanFactory 就像一个包含 bean 集合的工厂类。它会在客户端要求时实例化 bean。
- ApplicationContext - ApplicationContext 接口扩展了 BeanFactory 接口。它在 BeanFactory 基础上提供了一些额外的功能。
- Spring IoC 的实现机制:IoC 的实现原理就是工厂模式加反射机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26interface Fruit {
public abstract void eat();
}
class Apple implements Fruit {
public void eat(){System.out.println("Apple");}
}
class Orange implements Fruit {
public void eat(){System.out.println("Orange");}
}
class Factory {
public static Fruit getInstance(String ClassName) {
Fruit f=null;
try {
f=(Fruit)Class.forName(ClassName).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
class Client {
public static void main(String[] a) {
Fruit f=Factory.getInstance("io.github.dunwu.spring.Apple");
if(f!=null){f.eat();}
}
}
Ioc的底层实现
1、先通过createBeanFactory创建出一个Bean工厂(DefaultListableBeanFactory)
2、开始循环创建对象,因为容器中的bean默认都是单例的,所以优先通过getBean,doGetBean从容器中查找,找不到的话
3、通过createBean,doCreateBean方法,以反射的方式创建对象,一般情况下使用的是无参的构造方法(getDeclaredConstructornewinstance)
4、进行对象的属性填充populateBean
5、进行其他的初始化操作(initializingBean)ApplicationContext 和 BeanFactory 有什么区别?
Beanfactony是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而AppitcationContext继承了Beanfactony,所以ApplicationContext拥有Beanfacton所有的特点,也是一个Bean工厂,但是AppicationContext除开继承了Beanfacton之外,还继承了诸如EnvronmentCapable、Messaaesource、AppicationEventPubishe等接口,从而ApplicationCcontext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的.
Application contexts 提供一种方法处理文本消息,一个通常的做法是加载文件资源(比如镜像),它们可以向注册为监听器的 bean 发布事件。另外,在容器或容器内的对象上执行的那些不得不由 bean 工厂以程序化方式处理的操作,可以在Application contexts 中以声明的方式处理。Application contexts 实现了MessageSource 接口,该接口的实现以可插拔的方式提供获取本地化消息的方法。ApplicationContext 通常的实现是什么?
- FileSystemXmlApplicationContext:此容器从一个 XML 文件中加载 beans 的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
- ClassPathXmlApplicationContext:此容器从一个 XML 文件中加载 beans 的定义,你需要正确设置 classpath 因为这个容器将在 classpath里找 bean 配置。
- WebXmlApplicationContext:此容器加载一个 XML 文件,此文件定义了一个 WEB 应用的所有 bean。
Bean Factory 与 FactoryBean 有什么区别?
相同点:都是用来创建bean对象的
不同点:使用BeanFactory创建对象的时候,必须要道循严格的生命周期流程,太复杂了,,如果想要简单的自定义某个对象的创建,同时创建完成的对象想交给spring来管理,那么就需要实现FactroyBean接口了(Bean Factory标准化,FactoryBean个性化)
issingleton:是否是单例对象、getObjectType:获取返回对象的类型、getobject:自定义创建对象的过程(new,反射,动态代理)Spring容器启动流程是怎样的
1,在创建Spring容器,也就是启动Spring时:
2,首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
3,然后筛选出非懒加弱的单例BeanDefinton进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDeinition去建
4,利用BeanDefinition创建Bean就是Bean的创建生命因期,这期间包括了合并BeanDefintion、推断构造法、实例化、属性填充、初始化前、初始化、初始化后等步骤其中AOP就是发生在初始化后这一步骤中
5,单例Bean创建完了之后,Spring会发布一个容器启动事件
6,Spring启动结束
7,在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanfatoyPostProcessor和BeanPostprocessor的注册,Spring的描就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的8。在sprina启动过程中还会去处理@Import等注解Spring 支持集中 bean scope(范围)?
- Spring bean 支持 5 种 scope:
- Singleton - 每个 Spring IoC 容器仅有一个单实例。
- Prototype - 每次请求都会产生一个新的实例。
- Request - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。
- Session - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前HTTP session 内有效。
- Global-session - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于portlet 的 web 应用中才有意义。
??? Portlet规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。在 globalsession 作用域中定义的 bean 被限定于全局 portlet Session的生命周期范围内。如果你在 web 中使用global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。
仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。
- Spring bean 支持 5 种 scope:

??单例Bean和单例模式?什么是 spring 的内部 bean?
单例模式表示JVM中某个类的对象只会存在唯一一个。而单例Bean并不表示JVM中只能存在唯一的某个类的Bean对象。
只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 或 中提供了 元素的使用。内部 bean 总是匿名的,它们总是作为原型。SpringBean生命周期原理。
- 实例化Bean:反射的方式创建对象
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。
对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。 - 设置对象属性(依赖注入):populateBean(),循环依赖的问题(三级缓存)
实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成依赖注入。 - 处理Aware接口:接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean。invokeAwareMethod(完成BeanName,BeanFactory,BeanClassLoader对象的属性设置)
- 如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
- 如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
- 如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;
- BeanPostProcessor:如果想对Bean自定义处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。
使用比较多的有(ApplicationContextPostProcessor,设置ApplicationContext,Environment,ResourceLoader,EmbeddValueResolver等对象) - InitializingBean 与 init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
invokenitmethod(),判断是否实现了initializingBean接口,如果有,调用afterPropertiesset方法,没有就不调用 - 调用BeanPostProcessor的后置处理方法:若存在与 bean 关联的任何 BeanPostProcessors,则将调用 postProcessAfterInitialization()方法。
spring的aop就是在此处实现的,AbstractAutoProxyCreator。注册Destuction相关的回调接口:钩子函数 - 获取到完整的对象,可以通过getBean的方式来进行对象的获取
- DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;
- destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
- 实例化Bean:反射的方式创建对象
Spring中Bean是线程安全的吗
Spring本身并没有针对Bean做线程安全的处理,所以:
1,如果Bean是无状志的,那么Bean则是线程安全的
2,如果Bean是有状态的,那么Bean则不是线程安全的
另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线挥安全的还得看这个Bean本身。Spring 是如何解决循环依赖问题的?
如果两个或多个 Bean 互相之间持有对方的引用就会发生循环依赖。循环的依赖将会导致注入死循环。这是 Spring 发生循环依赖的原因。循环依赖有三种形态:
https://www.bilibili.com/video/BV1Mc411S7B7?p=12&vd_source=ff210768dfaee27c0d74f9c8c50d7274
、、三级缓存的作用是什么?Spring 中哪些情况下,不能解决循环依赖问题?
p156
、、缓存的放置时间、删除时间,,
https://www.bilibili.com/video/BV1Mc411S7B7?p=14&vd_source=ff210768dfaee27c0d74f9c8c50d7274Spring AOP
AOP(Aspect-Oriented Programming), 即 面向切面编程, 它与OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与OOP 不同的抽象软件结构的视角. 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)
Spring AOP 解决的是「重复性」的非业务代码抽取的问题。所谓「面向切面编程」在我理解下其实就是在方法前后增加非业务代码,将那些与业务无关,却为业务模块所同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
AOP 底层的技术是动态代理,???在Spring内实现依赖的是BeanPostProcessor。如果要代理的对象实现了某个接口,那么SpringAOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用IDK动态代理,转而使用CGlib动态代理生成一个被代理对象的子类来作为代理。AOP 可以用于的一些常见场景:
- 权限控制: AOP 可以用于在方法执行前或执行后添加权限控制的逻辑,例如检查用户是否有足够的权限执行某个操作。这样可以避免将权限检查逻辑散布在业务代码的各个地方。
- 事务控制: AOP 可以用于管理事务,例如在方法执行前开启事务,在方法执行后根据执行结果决定是提交事务还是回滚事务。这样可以确保事务的一致性和可靠性。
- 日志记录: AOP 可以用于添加日志记录逻辑,例如在方法执行前记录方法的输入参数,方法执行后记录返回结果。这样可以方便地进行日志管理和分析。
- 性能监控: AOP 可以用于添加性能监控逻辑,例如在方法执行前记录开始时间,在方法执行后记录结束时间,计算方法的执行时间。方便进行性能分析和优化。
- 异常处理: AOP 可以用于添加异常处理逻辑,例如捕获方法执行过程中的异常并进行统一处理。这样可以减少业务代码中的异常处理逻辑,提高代码的清晰度。
- 缓存管理: AOP 可以用于添加缓存管理逻辑,例如在方法执行前检查是否有缓存命中,如果有直接返回缓存结果,否则执行方法并将结果缓存起来。
AspectJ?Spring AOP和AspectJ AOP有什么区别?
- AOP是一种思想,AspectJ是一种实现。SpringAOP中已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。
- SpringAOP是属于运行时增强,而Aspect是编译时增强。 SpringAOP基于代理(Proxying),而AspectJ基于字节码操作(BytecodeManipulation)。
SpringAOP已经集成了AspectJ,Aspect应该算得上是Java生态系统中最完整的AOP框架了。Aspect相比于SpringAOP功能更加强大,但是SpringAOP相对来说更简单。
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择AspectJ,它比SpringAOP快很多。
什么是 Aspect(切面)?
Aspect 由 pointcount 和 advice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑编织到切面所指定的连接点中. 简单地认为, 使用 @Aspect 注解的类就是切面.
AOP 的工作重心在于如何将增强编织目标对象的连接点上, 这里包含两个工作:
1、如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
2、如何在advice 中编写切面代码.在SpringAOP中,关注点和横切关注的区别是什么?什么是连接点呢?切入点是什么?什么是通知呢,有哪些类型呢?
- 关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。
- 横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输。
- 连接点代表一个应用程序的某个位置,在这个位置我们可以插入一个AOP切面,它实际上是个程序执行SpringAOP的位置。
- 切入点(JoinPoint)是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方 式指明切入点。
- 通知(Advice)是个在方法执行前或执行后要做的动作(特定 JoinPoint 处的 Aspect 所采取的动作),实际上是程序执行时要通过SpringAOP框架触发的代码段。Spring切面可以应用五种类型的通知:
- Before:这类 Advice 在 joinpoint 方法之前执行,并使用@Before 注解标记进行配置。
- After Returning:这类型 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。
- After Throwing:这些类型的 Advice 仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 标记配置时执行。
- After (finally): 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。
- Around:在连接点之前和之后执行,并使用@Around 注解标记进行配置。
AOP 有哪些实现方式?
- 静态代理:使用 AOP 框架提供的命令进行编译,在编译阶段就可生成 AOP 代理类,因此也称编译时增强;
- 编译时编织(特殊编译器实现)
- 类加载时编织(特殊的类加载器实现)。
- 动态代理:在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
- JDK 动态代理
- CGLIB
- Spring AOP and AspectJ AOP 有什么区别?
Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。
???SpringAOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。 - 什么是编织(Weaving)?
为了创建一个 advice 对象而链接一个 aspect 和其它应用类型或对象,称为编织(Weaving)。在 Spring AOP 中,编织在运行时执行。 - 如何理解 Spring 中的代理?
???将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的。
- 静态代理:使用 AOP 框架提供的命令进行编译,在编译阶段就可生成 AOP 代理类,因此也称编译时增强;
??? SpringAOP的底层实现
- 总: aop概念,应用场景,动态代理
- 分: bean的创建过程中有一个步骤可以对bean进行扩展实现,aop本身就是IOC 的一个扩展功能,所以在BeanPostProcessor的后置处理方法中来进行实现
1、代理对象的创建过程(advice,切面,切点)
2、通过jdk或者cglib的方式来生成代理对象
3、在执行方法调用的时候,会调用到生成的字节码文件中,直接回找到DynamicAdvisoredlnterceptor类中的intercept方法,从此方法开始执行
4、根据之前定义好的通知来生成拦截器链
5、从拦截器链中依次获取每一个通知开始进行执行,在执行过程中,为了方便找到下一个通知是哪个,会有一个CglibMethodlnvocation的对象,找的时候是从-1的位置一次开始查找并且执行的。
在工作中实际用到过AOP优化代码吗???
有的。当时我用AOP来对我们公司现有的监控客户端进行封装
一个系统离不开监控,监控基本的指标有QPS、RT、ERROR等等。对外暴露的监控客户端只能在代码里写对应的上报信息(灵活,但会与业务代码掺杂在一起),于是我利用注解+AOP的方式封装了一把,只要方法/类上带有我自定义的注解;方法被调用时,就会上报AQS、RT等信息
实现了非业务代码与业务代码分离的效果(:Spring DAO
Spring DAO 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。Spring事务管理:
- Spring框架提供的一种事务管理机制,它简化了事务管理的操作,并提供了对不同事务管理器的统一接口。两种方式:
- 声明式事务:通过在方法上使用 @Transactional 注解来声明事务。这种方式更为常用,允许开发者将精力集中在业务逻辑上,而不需要关心事务的开始、提交或回滚。
1
2
3
4
public void myTransactionalMethod() {
// 业务逻辑
} - 编程式事务:通过编写代码手动管理事务的开始、提交或回滚。虽然不太常用,但在一些特殊情况下可能会用到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private PlatformTransactionManager transactionManager;
public void myProgrammaticMethod() {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 业务逻辑
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
Spring事务是如何回滚的?
- 即:Spring事务管理是怎么实现的?
考虑数据库事务:建立连接,开启事务->进行sql操作->成功,commit/失败,rollback,,发现sql操作就是业务逻辑,前后工作完全类似AOP的around - 总:spring的事务是由aop来实现的,首先要生成具体的代理对象,然后按照aop的整套流程来执行具体的操作逻辑,正常情况下要通过通知来完成核心功能,但是事务不是通过通知来实现的,而是通过一个TansactionInterceptor来实现的,然后调用invoke()实现具体的逻辑
- ???分:
1、先做准备工作,解析各个方法上事务相关的属性,根据具体的属性来判断是否开始新事务
2、当需要开启的时候,获取数据库连接,关闭自动提交功能,开起事务
3、执行具体的sql逻辑操作
4、在操作过程中,如果执行失败了,那么会通过completeTransactionAterThrowing看来完成事务的回滚操作,回滚的具体逻辑是通过doRollBack方法来实现的,实现的时候也是要先获取连接对象,通过连接对象来回滚
5、如果执行过程中,没有任何意外情况的发生,那么通过commitTransactionAfterReturning来完成事务的提交操作,提交的具体逻辑是通过doCommit方法来实现的,实现的时候也是要获取连接,通过连接对象来提交
6、当事务执行完毕之后需要清除相关的事务信息cleanupTransactionlnfo
如果想要聊的更加细致的话,需要知道Transactionlnfo,Transactionstatus,
- 即:Spring事务管理是怎么实现的?
Spring中的事务是如何实现的
1,Spring事务底层是基于数据库事务和AOP机制的
2,首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean
3,当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
4,如果加了,那么则利用事务管理器创建一个数据库连接、
5,并且修改数据库连接的autocommit属性为false,禁止此连接的白动提交,这是实现Spring事务非常重要的一步
6,然后执行当前方法,方法中会执行sql
7。执行完当前方法后,如果没有出现异常就直接提交事务
8,如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9,Spring事务的隔离圾别对应的就是数据库的隔离级别
10,Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
11,Spring事务的传机制是基于数据库连接来做的,一个数据库连接一个事务,如果传机制配置为需要新开一个事务,那么实际上就是先建立一个数库连接,在此新数据库连接上执行sqlSpring事务传播机制
多个事务方法相互调用时,事务如何在这些法间传摇,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。
1,REQUIRED(Spring默认的事务传播类型): 如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
2,SUPPORTS:当前存在事务,则加加入当前事务,如果当前没有事务,就以非事务方法执行
3。MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
4,REQUIRES_NEW: 创建一个新事务,如果存在当前事务,则挂起该事务。
5,NOT_SUPPORTED: 以非事务方式执行如里当前存在事务,则持起当前事务
6,NEVER:不使用事务,如果当前事务存在,则抛出异常
7,NESTED: 如果当前事存在,则在嵌套事务中执行,否则REQUIRED的操作一样 (开启一个事务)
https://www.bilibili.com/video/BV1Mc411S7B7?p=18&vd_source=ff210768dfaee27c0d74f9c8c50d7274Spring事务什么时候会失效?
spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了! 常见情况有如下几种
??1、发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身!解决方法很简单,让那个this变成UserService的代理类即可!
2、方法不是public的:@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 Aspectj 代理楼式
3、数据库不支持事务
4、没有被spring管理
5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)Spring中常见的设计模式。
- 单例模式:Spring默认情况下,容器中的Bean是单例的,即每个容器中的Bean只有一个实例。有助提高性能和减少资源消耗
- 原型模式:指定作用域为prototype
- 工厂模式:Spring通过工厂模式实现了IOC(Inversion of Control)容器,使得对象的创建和管理被委托给了Spring容器。 BeanFactory
- 代理模式:Spring AOP就是通过代理模式实现的。AOP允许开发者在不修改原始代码的情况下,插入和控制横切关注点,如日志、事务等。
- 责任链模式:使用aop时先生成一个拦截器链
- 模板方法模式: 在Spring中,JdbcTemplate 和 RedisTemplate 等模板类使用了模板方法模式,将通用的任务实现在模板方法中,而将具体实现留给子类。
postProcessBeanFactory,onRefresh,initPropertyValue - 观察者模式:Spring的事件机制是基于观察者模式的。通过定义事件和监听器,应用程序可以订阅感兴趣的事件,从而实现松耦合的通信机制。
- 策略模式:Spring的资源加载、事务管理等功能用了策略模式,通过定义一组算法族分别封装起来,并使它们可以相互替换。
- 适配器模式:SpringAOP中通知(Advice)就是一种适配器模式的应用。通知包装了一个切面逻辑,使得它可以在切点(Join Point)上执行。
- 装饰者模式:BeanWrapper
- 适配器模式:Adapter
Spring MVC
什么是 MVC?
MVC是一种设计模式,解决表现层的问题;MVC 模式有助于分离应用程序的不同方面,如输入逻辑,业务逻辑和 UI 逻辑,同时在所有这些元素之间提供松散耦合。
Model:模型(完成业务逻辑:有javaBean构成,service+dao+entity)
Controller:控制器(接收请求->调用模型->根据结果派发页面),SpringMVC通过一套注解,可以让普通的JAVA类成为contrlor控制器,无需继承Servlet
View:视图层(将结果渲染,相应给客户)Spring MVC 框架
SpringMVC是一个MVC的开源框架,提供 模型-视图-控制器 架构和随时可用的组件,用于开发灵活且松散耦合的 Web 应用程序。
SpringMVC=struts2+spring,springMVC就相当于是Struts2 加上sring的整合;SpringMVC是spring的一个后续产品,其实就是spring在原有基础上,又提供了web应用的MVC模块,可以简单的把springMVC理解为是spring的一个模块(类似AOP,IOC这样的模块)???Spring MVC 流程
https://www.bilibili.com/read/cv26597569/?spm_id_from=333.999.0.0&jump_opus=1
1、向服务器发送 HTTP 请求,请求被前端控制器 DispatcherServlet 捕获。
2、 DispatcherServlet 根据 -servlet.xml 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用HandlerMapping获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 对象的形式返回。
3、 DispatcherServlet 根据获得的 Handler,选择一个合适的HandlerAdapter。(附注:如果成功获得 HandlerAdapter 后,此时将开始执行拦截器的 preHandler(…)方法)。
4、提取 Request 中的模型数据,填充 Handler 入参,开始执行 Handler( Controller)。在填充 Handler 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: HttpMessageConveter:将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 数据转换:对请求消息进行数据转换。如 String 转换成 Integer、Double 等。 数据根式化:对请求消息进行数据格式化。如将字符串转换成格式化数字或格式化日期等。 数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResult 或 Error 中。
5、Handler(Controller)执行完成后,向 DispatcherServlet 返回一个ModelAndView 对象;
6、根据返回的 ModelAndView,选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的 ViewResolver)返回给DispatcherServlet。
7、 ViewResolver 结合 Model 和 View,来渲染视图。
8、视图负责将渲染结果返回给客户端。SpringMVC 常见注解
@RequestMapping:用于处理请求url映射的注解,可用于类或方法上。用于类上,则表示类中 的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为ison对象响应给客户
、、、
Spring Boot
什么是 Spring Boot?
随着新功能的增加,spring 变得越来越复杂。如果必须启动一个新的 Spring 项目,我们必须添加构建路径或添加Maven 依赖关系,配置应用程序服务器,添加 spring 配置,即开始一个新的 spring 项目需要很多努力,因为我们现在必须从头开始做所有事情。
Spring Boot 最重要的就是起步依赖和自动配置: 使用starter
启动器能够自动依赖其他组件,减少了Maven配置的繁琐性;Spring Boot根据当前类路径和jar包自动配置bean,例如,添加spring-boot-starter-web
启动器即可拥有web功能,无需额外的配置。因此,Spring Boot 可以帮助我们以最少的工作量,更加健壮地使用现有的 Spring功能。为什么要用SpringBoot?
- 独立运行和简化部署: Spring Boot内嵌了各种servlet容器(如Tomcat、Jetty、Undertow),使得应用能够独立运行,无需打成war包部署到外部容器。通过打包成可执行的jar包,所有依赖包都在一个jar包内,简化了部署和运维。
- 简化配置和自动配置: 使用
starter
启动器能够自动依赖其他组件,减少了Maven配置的繁琐性。Spring Boot根据当前类路径和jar包自动配置bean,例如,添加spring-boot-starter-web
启动器即可拥有web功能,无需额外的配置。 - 无代码生成和XML配置: Spring Boot的配置过程中无需代码生成,也不需要XML配置文件,通过条件注解实现自动配置,进一步简化了项目配置。
- 应用监控和健康检测: Spring Boot提供了一系列端点用于监控服务和应用,实现健康检测等功能。
- 减少开发、测试时间和努力: 提供快速启动开发的默认值,减少了开发和测试的时间和工作量。
- 使用JavaConfig避免XML配置: 倡导使用JavaConfig替代XML配置,提高了代码的可读性和维护性。
- 提供意见发展方法: Spring Boot提供了一套推荐的开发方法,使得团队更容易达成共识,提高了代码的一致性和规范性。
- 基于环境的配置: 支持基于环境的配置,通过传递环境参数(例如
-Dspring.profiles.active={environment}
),可以在不同环境中灵活配置应用程序。
如何创建 Spring Boot Projects ?
- Spring Initiatlizr 让创建 Spring Boot 项目变的很容易,通过 start.spring.io 创建。
- 手动设置一个 Maven 项目为 Spring Boot 项目:
- 添加 Spring Boot 依赖: 在 Maven 项目的
pom.xml
文件中,添加 Spring Boot 的依赖。通常,你可以添加spring-boot-starter
或其他特定的 Starter 依赖,具体依赖根据项目需求而定。1
2
3
4
5
6
7
8<dependencies>
<!-- Spring Boot Starter 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 其他依赖 -->
</dependencies> - 添加 Spring Boot 插件:在
pom.xml
中添加 Spring Boot Maven 插件。插件用于打包和运行 Spring Boot 应用程序。1
2
3
4
5
6
7
8<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> - 创建 Spring Boot 主类: 在项目中创建一个包含
main
方法的主类,该类用于启动 Spring Boot 应用程序。这个类通常被注解为@SpringBootApplication
。 - 添加其他 Spring Boot 配置: 根据项目需求,配置其他 Spring Boot 特性,如数据源、JPA、Web 支持等。可以通过配置文件(
application.properties
或application.yml
)或 Java 配置类进行配置。
- 添加 Spring Boot 依赖: 在 Maven 项目的
Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是 @SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下3个注解:
1、@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
2、@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能:@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
3、@ComponentScan:Spring组件扫描Spring Boot中常用注解及其底层实现
- @Bean注解:用来定义Bean,类似于XML中的bean>标签,Spring在启动时,会对加了@Bean注解的方法进行折,将方法的名字做为beanName,并通过执行方法得到bean对象
- Bean处理
- @Autowired:依赖注入
- @Component:泛指组件
- @Controller、@Service、@Repository
- @RestController
- @Configuration
- Http请求:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping
- 前后端参数传递:
- @RequestParam:用在方法的参数前面,获取请求中表单类型的key=value格式的数据
- @PathVariable:路径变量,参数与大括号里的名字一样要相同
- @RequestBody:获取请求body中的数据,常用于搭配@PostMapping请求来提交对象数据
- @ResponseBody:表示该方法的返回结果直接写入HTTP response body中,格式为json
- 读取配置
- @value:直接读取各种配置源的属性名
- @ConfigurationProperties:读取配置信息并与 bean 绑定
- @PropertySource:指定加载自定义的配置文件
- 参数校验
- Bean字段验证注解:@Null、@Min()、,,??
- ??@Valid:用于标注验证对象的级联属性
- ??@Validated:Spring提供的注解,于SpringMVC一起使用标注方法参数需要检查
- 统一异常处理
- @ControllerAdvice:注解定义全局异常处理类,包含@Component所以可以被Spring扫描到
- @ExceptionHandler:注解声明异常处理方法,表示遇到这个异常,就执行标注的方法
- JPA数据持久化
- @Entity:声明数据库实体类
- @Table:设置表明
- @ld:声明一个字段为主键
- @GeneratedValue:声明主键的生成策略
- @Column:声明字段,经常用于属性名和表字段的映射
- @Transient:指定不需要持久化的字段
- @Lob:声明某个字段为大字段
- @Enumerated:声明枚举类型的字段
- @Modifying:加在DAO方法上,提示是修改操作
- @Transactional
- 作用于类上:表示所有该类的public 方法都配置相同的事务属性信息
- 作用于方法上:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息
- 测试处理
- @ActiveProfiles:常作思于测试类上,用于声明生效的 Spring 配置文件
- @Test:声明一个方法为测试方法
- @Transactional:被声明的测试方法的数据会回滚,避免污染测试数据
- @WithMockUser:Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限
- 配置启动
- @SpringBootApplication注解: 这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合:
a,@SpringBootConfiguration: 这个注解实际就是一个@Configuration,表示启动类也是一个配置类
b,@EnableAutoConfiguration:向Spring容器中导入了一Seletor,用来加ClassPath下SpringFactoties中所定义的自动配置类,将这些自动为配置Bean
c,@ComponentScan:标识扫描路径,因为赋认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录 - @Conditional
- @ConditionalOnBean:配置了某个特定的Bean
- @ConditionalOnMissingBean:没有配置特定的Bean
- @ConditionalOnClass:Classpath里有指定的类
- @ConditionalOnMissingClass:Classpath里没有指定的类
- @ConditionalOnExpression:给定的SpEL表达式计算结果为true
- @ConditionalOnJava:Java的版本匹配特定值或者一个范围值
- @ConditionalonIndi:参数中给定的INDI位置必须存在一个,如果没有给参数,则要有JNDIInitialContext
- @ConditionalOnProperty:指定的配置属性要有一个明确的值
- @ConditionalOnResource:Classpath里没有指定的资源
- @ConditionalOnWebApplication:这是一个Web应用程序
- @ConditionalOnNotWebApplication:这不是一个Web应用程序
- @SpringBootApplication注解: 这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合:
??说说SpringBoot自动配置。
1、SpringBoot有着“约定大于配置”的理念,这一理念一定程度上可以用“SpringBoot自动配置”来解释。
通过 Spring Boot,我们可以快速开发基于 Spring 生态下的应用程序。
2、Spring Boot 约定由于配置的体现有很多,比如:
Spring Boot Starter 启动依赖,它能帮我们管理所有 jar 包版本;
如果当前应用依赖了 spring mvc 相关的 jar,那么 Spring Boot 会自动内置Tomcat 容器来运行 web 应用,我们不需要再去单独做应用部署;
Spring Boot 的自动装配机制的实现中,通过扫描约定路径下的 spring.factories 文件来识别配置类,实现 Bean 的自动装配;
默认加载的配置文件 application.properties 等等
3、在使用SpringBoot的时候,肯定会依赖于autoconfigure这么一个包,autoconfigure这个包里会有一个spring.factories文件,该文件定义了100+个入口的配置类。比如我们经常使用的redis、kafka等等这样常见的中间件都预置了配置类。当我们在启动SpringBoot项目的时候,内部就会加载这个spring.factories文件,进而去加载“有需要”的配置类。那我们在使用相关组件的时候,就会非常的方便(因为配置类已经初始化了一大部分配置信息)。一般我们只要在application配置文件写上对应的配置,就能通过各种template类直接操作对应的组件啦。
4、不是所有的配置类都会加载的,假设我们没有引入redis-starter的包,那Redis的配置类就不会被加载。具体Spring在实现的时候就是使用@ConditionalXXX
进行判断的。比如Redis的配置类就会有@ConditionalOnClass({RedisOperations.class})的配置,说明当前环境下如果有RedisOperations.class这个字节码,才会去加载Redis的配置类。??Spring Boot 中自动装配机制的原理。
自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置。在 Spring Boot 应用里面,只需要在启动类加上 @SpringBootApplication 注解就可以实现自动装配。
@SpringBootApplication 是一个复合注解,真正实现自动装配的注解是 @EnableAutoConfiguration。自动装配的实现主要依靠三个核心关键技术。
1、引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration 配置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对象。
2、这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:/META-INF/spring.factories 文件中。这样 SpringBoot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主要是用到了 Spring 里面的 SpringFactoriesLoader 来完成的。
3、SpringBoot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的ImportSelector 接口,实现对这些配置类的动态加载。
在我看来,SpringBoot 是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和业务无关的配置。其实,自动装配的思想,在 SpringFramework3.x 版本里面的@Enable 注解,就有了实现的雏形。@Enable 注解是模块驱动的意思,我们只需要增加某个@Enable 注解,就自动打开某个功能,而不需要针对这个功能去做 Bean 的配置,@Enable 底层也是帮我们去自动完成这个模块相关 Bean 的注入。以上,就是我对 Spring Boot 自动装配机制的理解。Spring Boot 属性源
1.命令行参数
2.JVM系统属性
3.操作系统环境变量
4.打包在应用程序内的 application.properties 或者 application.yml 文件
5.通过 @PropertySource 标注的属性源
6.默认属性你对SpringBoot starter的理解?
Starters是什么:Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成Spring及其他技术,而不需要到处找示例代码和依赖包。
比如 Mybatis 框架会需要引入各种的包才能使用,而starter就是做了一层封装,把相关要用到的jar都给包起来了,并且也写好了对应的版本。这我们使用的时候就不需要引入一堆jar包且管理版本类似的问题了。
Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。常用的starter。
spring-boot-starter-web 嵌入tomcat和web开发需要servlet与jsp支持
spring-boot-starter-data-jpa 数据库支持
spring-boot-starter-data-redis redis数据库支持
spring-boot-starter-data-solr solr支持
mybatis-spring-boot-starter 第三方的mybatis集成starterSpring Boot是如何启动Tomcat的
1,首先,SpringBoot在启动时会先创建一个Spring容器
2,在创建 Spring容器过程中,会利用 @ConditionalOnClass 技术来判断当前 classpath中是否存在 Tomcat 依赖,如果存在则会生成一个启动 tomcat 的Bean
3,Spring 容器创建完之后,就会获取启动 Tomcat 的Bean,并创建 Tomcat 对象,并绑定端口等,然后启动 Tomcat
( Spring Boot内嵌tomcat,与springmvc启动tomcat过程相反?????Spring Boot中的监视器是什么?如何在 Spring Boot 中禁用 Actuator 端点安全性?
Spring boot actuator是spring启动框架中的重要功能之一。Spring boot监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为HTTP URL访问的REST端点来检查状态
默认情况下,所有敏感的 HTTP 端点都是安全的,只有具有 ACTUATATOR 角色的用户才能访问它们。安全性是使用标准的HttpServletRequest.isUserInRole 方法实施的。 我们可以使用来禁用安全性。只有在执行机构端点在防火墙后访问时,才建议禁用安全性。如何使用Spring Boot实现异常处理?
Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。 我们通过实现一个ControlerAdvice类,来处理控制器类抛出的所有异常SpringBoot 实现热部署有哪几种方式?
主要有两种方式:Spring Loaded,Spring-boot-devtools如何实现 Spring Boot 应用程序的安全性?
为了实现 Spring Boot 的安全性,我们使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。Spring Boot 支持哪些日志框架?推荐和默认的日志框架是哪个?
Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用 Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架Spring Boot 2.X 有什么新特性?与 1.X 有什么区别?
配置变更
JDK 版本升级
第三方类库升级
响应式 Spring 编程
支持HTTP/2
支持配置属性绑定
更多改进与加强…SpringBoot 集成 Mybatis 的过程
添加mybatis的starter maven依赖 org.mybatis.spring.boot mybatis-spring-boot-starter 1.2.0
在mybatis的接口中 添加@Mapper注解
在application.yml配置数据源信息如何对Spring Boot应用进行测试?
在为Spring应用程序运行集成测试时,我们必须有一个 ApplicationContext。
为了简化测试,Spring Boot为测试提供了一个特殊的注释 @SpringBootTest。此批注从其 classes 属性指示的配置类创建 ApplicationContext。 如果未设置classes属性,Spring Boot将搜索主配置类。搜索从包含测试的包开始,直到找到使用@SpringBootApplication或@SpringBootConfiguration注释的类。
???请注意,如果我们使用 JUnit 4 ,我们必须用 @RunWith(SpringRunner.class) 装饰测试类。Spring Boot 项目结构
- Spring Boot 项目通常遵循一种约定大于配置的原则,并提供了一种建议的项目结构,使得开发者可以更轻松地组织和管理项目。以下是一个常见的 Spring Boot 项目结构示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17my-spring-boot-project
|-- src
| |-- main
| |-- java
| | |-- com
| | |-- example
| | |-- MySpringBootApplication.java
| | |-- controller
| | |-- HomeController.java
| | |-- service
| | |-- MyService.java
| |-- resources
| |-- application.properties
| | |-- static
| | |-- templates
|-- target
|-- pom.xml src/main/java
: 存放 Java 源代码。com.example.MySpringBootApplication
: Spring Boot 应用程序的主类,包含main
方法,用于启动应用程序。com.example.controller
: 存放控制器类,处理 HTTP 请求。com.example.service
: 存放服务类,处理业务逻辑。src/main/resources
: 存放资源文件。application.properties
: Spring Boot 应用程序的配置文件,用于配置各种属性。static
: 存放静态资源文件,如 CSS、JavaScript 等。templates
: 存放模板文件,如 Thymeleaf 模板。target
: Maven 构建目录,包含编译后的类文件和构建产物。pom.xml
: Maven 项目的配置文件,定义项目的依赖和构建配置。
- Spring Boot 项目通常遵循一种约定大于配置的原则,并提供了一种建议的项目结构,使得开发者可以更轻松地组织和管理项目。以下是一个常见的 Spring Boot 项目结构示例:
最后聊下你是怎么看这块源码的???
思路:我先从启动类开始,会有个@SpringBootApplication,后面会定位到一个自动配置的注解@EnableAutoConfiguration,那最后就能看到注解内部会去META-INF/spring.factories加载配置类
SpringCloud
什么是 Spring Cloud?
Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。使用 Spring Cloud 有什么优势?
- 高可用性: 分布式架构能够提高系统的可用性。通过将系统划分为多个服务并分布在不同的节点上,即使某个节点或服务出现故障,整个系统仍然能够继续运行。
- 扩展性: 分布式系统可以更容易地进行横向扩展。通过增加节点或服务,系统可以更好地处理增加的负载,提高性能和吞吐量。
- 容错性: 分布式系统能够通过冗余和备份机制来提高容错性。即使某个节点或服务出现故障,备份节点或服务可以接管工作,保证系统的正常运行。
- 灵活性: 分布式系统能够更好地支持异构性,即不同类型的硬件、操作系统和编程语言。这使得系统更加灵活,能够选择最适合特定任务的技术栈。
- 资源共享: 分布式系统可以充分利用多个节点的计算和存储资源,实现资源的共享和最优化利用。
- 降低单点故障风险: 分布式系统通过将系统划分为多个部分,降低了单点故障的风险。即使某个节点或服务出现问题,其他部分仍然可以继续运行。
- 地理分布: 分布式系统支持地理分布,使得服务可以部署在不同的地理位置,提高服务的可用性和响应速度。
- 提高性能: 分布式系统可以通过并行计算和分布式存储来提高性能。任务可以同时在多个节点上执行,加速处理过程。
使用 Spring Boot 开发分布式微服务时,我们面临以下问题
1、与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。
2、服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。
3、冗余-分布式系统中的冗余问题。
4、负载平衡 –负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。
5、性能-问题 由于各种运营开销导致的性能问题。
6、部署复杂性-Devops 技能的要求。Spring Cloud 中的五大组件。
- Eureka(服务注册与发现): 服务注册与发现框架。它用于管理各个微服务的状态,以实现服务之间的通信和发现。
- Ribbon(负载均衡): Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,在服务消费者的请求中自动实现负载均衡,从而分摊到多个服务提供者上。
- Feign(声明式服务调用): Feign 是一个声明式的伪 HTTP 客户端,它使得编写 HTTP 客户端变得更加简单。通过 Feign,开发者只需使用注解方式就可以轻松地调用服务,而不用手动处理 HTTP 请求和响应。
- Hystrix(熔断器): 一种延迟和故障容错库,用于隔离访问远程服务、第三方库或者服务的点,防止故障蔓延到整个系统,从而提高系统的弹性和可用性。
- Zuul(API 网关): Zuul 是 Netflix 提供的一个基于 JVM 路由和服务端的负载均衡器,用于构建微服务架构中的 API 网关,对外统一提供服务访问入口,并提供了路由、负载均衡、熔断、安全等功能。
服务注册和发现是什么意思?Spring Cloud 如何实现?
当我们开始一个项目时,我们通常在属性文件中进行所有的配置。随着越来越多的服务开发和部署,添加和修改这些属性变得更加复杂。有些服务可能会下降,而某些位置可能会发生变化。手动更改属性可能会产生问题。 Eureka 服务注册和发现可以在这种情况下提供帮助。由于所有服务都在 Eureka 服务器上注册并通过调用 Eureka 服务器完成查找,因此无需处理服务地点的任何更改和处理。负载平衡的意义什么?
在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。什么是 Hystrix???
它如何实现容错?Hystrix 是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作什么是微服务
微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。
另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。什么是服务熔断?服务降级?
服务降级熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。
服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。说说 RPC 的实现原理
首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。REST 和RPC对比
1.RPC主要的缺陷是服务提供方和调用方式之间的依赖太强,需要对每一个微服务进行接口的定义,并通过持续继承发布,严格版本控制才不会出现冲突。
2.REST是轻量级的接口,服务的提供和调用不存在代码之间的耦合,只需要一个约定进行规范。微服务的优点缺点?说下开发项目中遇到的坑?
优点:1.每个服务直接足够内聚,代码容易理解2.开发效率高,一个服务只做一件事,适合小团队开发3.松耦合,有功能意义的服务。4.可以用不同语言开发,面向接口编程。5.易于第三方集成6.微服务只是业务逻辑的代码,不会和HTML,CSS或其他界面结合.7.可以灵活搭配,连接公共库/连接独立库
缺点:1.分布式系统的责任性2.多服务运维难度加大。3.系统部署依赖,服务间通信成本,数据一致性,系统集成测试,性能监控。Spring Cloud 和 Dubbo 有哪些区别?
Spring cloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo以开始是一个RPC调用框架,核心是解决服务调用间的问题,Springcloud是一个大而全框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有Springcloud全面,但Dubbo的服务调用性能Springcloud高,不过并不对立的,是可以结合起来一起使用的。Eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?
Zookeeper保证了CP(C:一致性,P:分区容错性),Eureka保证了AP(A:高可用)
1.当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。
2.Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:
①、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。
②、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)
③、当网络稳定时,当前实例新的注册信息会被同步到其他节点。因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪Ribbon和Feign的区别?
1.都是调用其他服务的,但方式不同。
2.启动类注解不同,Ribbon是@RibbonClient,feign的是@EnableFeignClients
3.服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。
4.调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTeTemplate发送给其他服务,步骤相当繁琐。Feign需要将调用的方法定义成抽象方法即可。Spring Cloud Gateway?
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。
使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。什么是 Ribbon负载均衡?
1.Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。
2.Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。