文章目录
- Java 基础篇
- 一、面向对象的理解
- 二、equals 和 == 的区别
- 三、什么是高内聚低耦合
- 四、public、private、protected,以及无修饰符的区别
- 五、介绍下HashCode
- 六、Java数据类型
- 七、装箱和拆箱
- 八、静态变量与实例变量的区别
- 九、String s = "" 与 new String() 的区别
- 十、Switch能否用String做参数
- 十一、什么是字节码?采用字节码的最大好处是什么?
- 十二、Java 中 final 关键字有什么⽤?
- 十三、HashCode与equals的区别?什么是Hash碰撞?
- 十四、为什么需要同时重写hashCode()和equals()?
- 十五、String 和 StringBuffer、StringBuilder 的区别是什么?
- 十六、什么是 Java 内部类? 内部类的分类有哪些 ?内部类有哪些优点和应⽤场景?
- 十七、抽象类与普通类的区别?
- 十八、接口和抽象类有什么区别?
- 十九、重载和重写
- 二十、Java运算符有哪些?
- 二一、JDK、JRE、JVM有什么区别?
- 二二、OOP和AOP的区别?
- 二三、介绍下AOP的核心概念
- 二四、什么叫序列化,怎么去序列化?
- Java 容器篇
- 一、请你说一下集合体系,从顶层说起
- 二、如何选用集合
- 三、LinkedList和ArrayList的区别
- 四、HashMap和HashTable的区别,哪个的性能更好?
- 五、HashSet和TreeSet的特征
- 六、HashSet如何检查重复
- 七、HashMap 和 ConcurrentHashMap 的区别?
- 八、Hashmap 的底层结构以及它的存储流程
- 九、HashMap的扩容机制
- 十、为啥HashMap每次扩容是扩容到原来的两倍?
- 十一、什么是泛型
- 十二、ConcurrentHashMap各版本的变化
- 十三、list()和Iterate()的区别?
- 十四、请你说一下List,Set,Map三个集合的区别?
- 十五、HashSet 和 HashMap的区别
- 十六、ArrayList是否会越界
- Java反射(Reflection)篇
- 并发编程篇
- 一、池化技术
- 二、有没有用过线程池?怎么配置的?有没有根据业务情况进行调整?
- 三、线程池中,有哪些参数,分别的作用是什么
- 四、保证多线程的线程安全有哪些方式
- 五、什么是线程死锁?怎么避免死锁,怎么解决死锁问题?
- 六、java线程唤醒和阻塞的五种常用方法
- 七、为什么`suspend()`和`resume()`被弃用?
- 八、wait()和sleep()的区别,sleep(0)可不可以有什么用
- 九、sleep(0)的作用
- 十、进程,线程和协程的区别
- 十一、实现Runnable接口和Callable接口的区别?
- 十二、为什么我们调用start方法会调用run()方法,而不能直接调用run方法?
- 十三、请简要介绍一下 synchronized 关键字和 ReentrantLock 的区别和适用场景。
- 十四、Synchronized的锁升级
- 十五、在并发编程中,锁的性能是一个重要的考量因素。你如何评估锁的性能?
- 十六、CAS原理,AQS原理
- 十七、Java原子类
- 十八、了解过Atomic操作类吗?底层是什么?
- 十九、介绍下Volatile关键字
- 二十、介绍下公平锁和非公平锁的区别?
- 二一、悲观锁和乐观锁
- 二二、Synchronized和volatile的区别是什么?
- 设计模式篇
- JAVA新特性
- 数据结构篇
- Java Web篇
- Java虚拟机篇
- 网络编程篇
- 数据库编程篇
- 一、MySQL有哪些数据类型?
- 二、什么是索引?
- 三、MySQL索引有哪些类型?
- 四、索引有哪些优缺点?
- 五、创建索引的三种方式?
- 六、大表如何添加索引
- 七、索引什么时候会失效?
- 八、哪些场景不适合建立索引?
- 九、索引下推了解过吗?什么是索引下推
- 十、聚簇索引与非聚簇索引的区别
- 十一、什么是覆盖索引?
- 十二、什么是回表?如何减少回表?
- 十三、B+树的底层是什么
- 十四、B+树有什么特点
- 十五、为什么要用 B+树,为什么不用二叉树或者其他树形结构?
- 十六、Hash 索引和 B+树区别是什么?你在设计索引是怎么抉择的?
- 十七、数据库三大范式
- 十八、MySQL常用的数据库引擎有哪些?
- 十九、如何选择引擎?
- 二十、Mysql的锁有哪些?
- 二一、InnoDB三种行锁的算法是什么?
- 二二、mysql开发中有没有遇到过死锁的情况
- 分布式系统篇
- 一、什么是分布式事务?
- 二、分布式事务的几种解决方案
- 三、Spring框架怎么管理事务,用到什么原理?
- 四、你知道哪几种声明式事务失效的场景吗?
- 五、Beanfactory和ApplicationContext有什么区别?
- 六、Bean的不同配置方式
- 七、Spring Bean的生命周期流程
- 八、谈谈你对spring框架的理解
- 九、Spring的三级缓存知道吗?
- 十、Spring的三级缓存解决循环依赖
- 十一、在Spring中,除了三级缓存机制,还有哪些其他方式可以解决循环依赖问题?
- 十二、Spring 的常用注解
- 十三、Spring MVC的常用注解
- 十四、Spring Security的常用注解
- 十五、SpringBoot的常用注解有哪些?
- 十六、Spring Boot Actuator 相关注解
- 十七、Spring和SpringBoot的自动装配了解过吗?
- 十八、谈谈你对SpringBoot的理解
- 十九、搭建Springboot框架时,框架的异常是怎么配的?
- 二十、Property yaml 文件里面配的参数可以通过哪些方式获取?
- 二一、SpringMVC的运作流程?
- 微服务架构篇
- 一、集群、分布式、微服务概念和区别?
- 二、传统的web服务于Restful风格的服务的区别?
- 三、RPC服务和Restful服务的区别?
- 四、Restful的六大原则?
- 五、Controller层输入规范
- 六、了解过SpringCloud的微服务嘛?了解过哪些组件?
- 七、请解释一下SpringCloud和Eureka在项目中的具体应用和优势,以及为什么选择这些技术作为微服务架构的基础
- 八、Spring Cloud 与 Dubbo的区别
- 九、在微服务架构中,服务之间的通信方式有哪些,它们的特点和适用场景分别是什么?
- 十、服务端微服务地址不小心暴露了,用户就可以绕过网关,直接访问微服务,怎么办?
- 十一、什么是三层架构,四层架构,六边形模型,分层架构,洋葱架构
- 十二、网关层中都做了什么?
- 十三、Eureka的服务注册过程
- 十四、什么是降级熔断?
- 十五、什么是限流?
- Redis 篇
Java 基础篇
一、面向对象的理解
面向对象是一种软件开发的思想和方法
,它将真实世界的事物抽象为对象,对象之间通过消息传递和方法调用进行交互,以此构建出具有灵活性和可维护性的软件系统。
在我的深刻思考中,我认为面向对象有以下一些重要特点和优势:
封装性
面向对象的封装性使得对象的内部细节对外部是隐藏的,只暴露必要的接口,这样可以降低模块之间的耦合度,提高系统的稳定性和安全性。
继承性
继承让对象之间建立了一种“is-a”的关系,可以实现代码的复用和层次化管理,减少重复代码,提高代码的维护性和可扩展性。
多态性
多态性使得不同类型的对象可以对同一消息产生不同的反应,这样可以提高代码的灵活性和可扩展性,实现更通用的代码设计。
抽象性
抽象性允许我们从具体事物中抽象出通用性特征和规律,从而进行更高层次的建模和设计。
模块化
面向对象的模块化设计让代码以相对独立的单元进行组织和管理,每个对象和类都具有独立的功能,易于理解和维护。
对我来说,面向对象编程思想的重要性在于它的灵活性和可扩展性,让软件的开发更加符合问题的本质,降低代码的复杂度和维护成本。
二、equals 和 == 的区别
在 Java 中,equals
方法和 ==
操作符都用于比较两个对象,但它们的含义和用途有所不同。
==
操作符
==
是一个操作符,不是方法。- 它比较两个对象的引用是否相同,也就是说,它们是否指向内存中的同一个对象。
- 对于基本数据类型(如 int、float、double、char、byte、short、long、boolean),
==
比较的是值是否相等。 - 对于对象,
==
比较的是对象的内存地址,即它们是否为同一个实例。
示例
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // 输出 false,因为 s1 和 s2 是两个不同的对象
System.out.println(s1.equals(s2)); // 输出 true,因为 "hello" 等于 "hello"
equals
方法
equals
是一个方法,需要调用对象来执行。- 它用于比较对象的内容是否相等。默认行为是比较对象引用,但可以被任何类重写以提供其他比较逻辑。
- 重写
equals
方法时,通常也应该重写hashCode
方法,以保持equals
和hashCode
的一致性。 - 对于字符串(String)类,
equals
方法被重写为比较字符串的内容。
示例
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // 输出 true,因为字符串字面量在 JVM 中是共享的
System.out.println(s1.equals(s2)); // 输出 true,因为 "hello" 内容等于 "hello"
最佳实践
- 当需要比较对象的逻辑内容时,使用
equals
方法。 - 当需要检查两个引用是否指向完全相同的对象时,使用
==
操作符。 - 在重写
equals
方法时,确保遵守等价关系原则:自反性、对称性、传递性、一致性,以及对于任何非空引用x
,x.equals(null)
应返回false
。
注意
- 在 Java 中,
==
操作符和equals
方法在比较字符串时的行为有所不同,这是由于 Java 为字符串类String
重载了==
操作符,比较的是字符串的内容而不是引用。其他对象类型使用==
默认是比较引用。
正确使用 equals
和 ==
对于编写正确、高效的代码非常重要。
三、什么是高内聚低耦合
高内聚低耦合是软件工程中两个重要的设计原则,它们通常用于指导模块、类或函数的设计,以提高代码的可维护性、可读性和灵活性。
耦合
元素与元素之间的连接。元素即是功能,对象,系统,子系统, 模块
存在A、B方法,当A元素去调用B元素时,当B元素不存在或者有问题,A元素就不能正常工作,那么就说元素A和元素B耦合。
高内聚(High Cohesion)
尽可能类的每个成员方法只完成一件事(最大限度的聚合)
高内聚意味着一个模块、类或函数集中于完成一组密切相关的功能。在高内聚的设计中,每个模块或类都有明确的职责,且该职责范围内的所有操作都是相关的。
特点:
- 单一职责:每个模块或类只负责一项功能或业务逻辑。
- 功能集中:模块内的方法或属性都紧密相关,共同协作以实现模块的职责。
- 易于理解:由于关注点集中,高内聚的模块通常更易于理解和维护。
低耦合(Low Coupling)
减少类内部,一个成员方法调用另一个成员方法
低耦合是指系统中的各个模块、类或组件之间的相互依赖性较低。在低耦合的设计中,每个模块都可以独立于其他模块进行开发、测试和维护。
特点:
- 独立性:模块之间的交互通过定义良好的接口进行,不依赖于其他模块的内部实现细节。
- 可重用性:低耦合的模块更容易在不同的项目中重用。
- 易于测试:由于模块间的依赖性低,单独测试模块的行为变得更加容易。
- 灵活性:在需要更改系统的一部分时,其他部分不受影响,从而提高了系统的灵活性。
如何实现高内聚低耦合?
- 单一职责原则:确保每个类或模块只负责一个特定的功能。
- 接口隔离原则:使用接口定义模块之间的契约,避免使用过于宽泛的接口。
- 依赖倒置原则:高层模块不应依赖于低层模块,两者都应该依赖于抽象。
- 使用设计模式:如观察者模式、策略模式等,可以减少模块间的直接依赖。
- 最小化依赖:尽量减少模块之间的直接引用,使用依赖注入等技术来提供所需的依赖。
- 抽象:通过抽象将变化隔离,使得模块可以独立于其他模块变化。
意义
- 提高可维护性:高内聚低耦合的设计使得代码更容易理解和维护。
- 增强灵活性:当需求变化时,可以更容易地修改或替换系统中的某些部分。
- 提高可测试性:模块间的低耦合使得单元测试和集成测试更加容易实现。
高内聚低耦合是软件设计追求的目标,有助于创建结构清晰、易于维护和扩展的系统。
耦合和内聚的评判标准时强度,耦合越弱越好,内聚越强越好。
四、public、private、protected,以及无修饰符的区别
在Java中,访问修饰符指的是控制类、接口、方法、属性等成员的访问范围。
public
:可以被任何类或对象访问private
:只能被定义该成员的类访问,其他类无法访问protected
:可以被当前类、子类和同一个包中的类访问默认(无修饰符)
:可以被同一个包中的类访问。
各个访问修饰符的特点
public
可以被任何类或对象访问,因此其访问范围最⼤,但也可能会存在安全问题。private
限制了访问范围,可以有效保护数据的安全,但是可能会增加代码的耦合度。protected
提供了⼀种在继承中使⽤的访问控制⽅式,但是可能会导致模块间的耦合。默认(无修饰符)
:访问范围⽐ protected 更⼩,只能被同⼀个包中的类访问,可以减⼩模块间的耦合。
总结
访问修饰符的选择需要根据具体情况来考虑,不能⼀概⽽论。通常情况下,应该尽可能地将成员设置为 private, 只在需要的情况下使⽤ public 或 protected。 需要注意的是,在同⼀个类中,成员可以直接访问其他成员,⽆论其访问修饰符是什么。
五、介绍下HashCode
HashCode
的特性
-
HashCode的存在主要用于查找的快捷性,如HashTable,HashMap等,HashCode经常用于确定对象的存储地址。
-
如果两个对象相同,equals方法一定返回true,并且这两个对象的HashCode一定相同。
-
两个对象的HashCode相同,并不一定表示两个对象就相同,即equals()不一定为true,只能够说明这两个对象在一个散列存储结构中。
-
如果对象的equals方法被重写,那么对象的HashCode也尽量重写。
HashCode
的源码
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
HashCode
的实现
一般情况下,hashCode()方法的默认实现会返回对象的内存地址的哈希码。但是,在某些情况下,我们可能需要自己实现hashCode()方法,以便更好地与其他对象进行比较和匹配。
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
return result;
}
hashCode()方法的实现应该保证相同的对象具有相同的哈希码,而不同的对象应该尽可能地有不同的哈希码。这是因为哈希码被用于数据结构中的键值对存储和查找操作,如果两个不同的对象具有相同的哈希码,就会出现哈希冲突,导致数据结构无法正常工作。
六、Java数据类型
Java 数据类型可以分为两大类:基本数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)。
一、基本数据类型
基本数据类型是 Java 语言预定义的,它们代表了最基本的数据单元,并且它们在内存中不通过引用间接寻址。Java 的基本数据类型包括:
-
整型:
byte
(1字节): -128 至 127short
(2字节): -32,768 至 32,767,即 -2^15 到 2^15 -1。int
(4字节): -2,147,483,648 至 2,147,483,647,即-2^31 到 2^31 -1。long
(8字节): -2^63 至 2^63-1,即 -2^63 到 2^63-1。
-
浮点型:
float
(4字节): 遵循 IEEE 754 标准的单精度浮点数,范围为 -2^31 到 2^31 -1,尾数精度为7位有效数字。double
(8字节): 遵循 IEEE 754 标准的双精度浮点数,范围为 -2^63 到 2^63 -1,尾数精度为15位有效数字。
-
字符型:
char
(2字节): 16位 Unicode 字符,可以存储任何字符,范围为0 到 2^15 -1。
-
布尔型:
boolean
(1字节): 只有两个可能的值:true
或false
二、引用数据类型
引用数据类型指向内存中的一个对象,它们是通过引用间接寻址的。Java 的引用数据类型包括:
-
类(Class):如用户定义的类类型,所有的类对象都是从java.lang.Object类派生的。
-
接口(Interface):由类实现的接口。
-
数组(Array):一种容器对象,可以包含固定数量的单一类型值。
三、默认值
Java 数据类型都有默认值,当变量被声明而没有被显式初始化时,它们将被赋予这些默认值:
- 对于
int
和short
,默认值是0
。 - 对于
long
,默认值是0L
。 - 对于
float
,默认值是0.0f
。 - 对于
double
,默认值是0.0d
。 - 对于
char
,默认值是'\u0000'
(即 Unicode 编码的空字符)。 - 对于
boolean
,默认值是false
。 - 对于引用数据类型,默认值是
null
。
四、选择数据类型
选择适当的数据类型通常取决于应用的需要:
- 使用基本数据类型来表示固定范围的数值,因为它们存储空间较小,且操作效率较高。
- 使用引用数据类型来表示更复杂的数据结构,如对象、数组和集合。
合理选择和使用数据类型对于编写高效、可读和可维护的代码至关重要。
七、装箱和拆箱
在计算机编程中,“装箱”(Boxing)和“拆箱”(Unboxing)是术语,主要用于讨论基本数据类型和它们的包装类(Wrapper classes)之间的转换。这个概念在多种编程语言中存在,但最常在讨论Java和C#等语言时使用。
装箱 (Boxing)
装箱是指将基本数据类型的值(如int
、double
、float
等)转换为对应的包装类类型(如Integer
、Double
、Float
等)的过程。在Java中,这是自动进行的,不需要显式地编写代码来完成。例如:
Integer myInteger = 10; // 自动装箱,将 int 转换为 Integer
拆箱 (Unboxing)
拆箱是装箱的逆过程,它将包装类类型的值转换回基本数据类型。这同样在Java中是自动进行的:
int myInt = myInteger; // 自动拆箱,将 Integer 转换为 int
性能考虑
虽然装箱和拆箱在语法上很便捷,但它们在性能上可能带来一些影响:
- 装箱 创建了一个新的对象,这在循环或频繁操作中可能导致过高的内存分配和垃圾收集开销。
- 拆箱 涉及对象的访问,这比直接使用基本类型要慢,因为它需要更多的内存和CPU周期。
最佳实践
由于性能的原因,建议在性能敏感的应用程序中谨慎使用装箱和拆箱:
- 避免在循环内部进行不必要的装箱和拆箱操作。
- 优先使用基本数据类型而不是它们的包装类,特别是作为集合的元素或数组的元素。
- 使用诸如
HashMap
的Integer
键时,要意识到这实际上是在用Integer
的实例,而不是原始的int
。
在Java 8及以后的版本中,通过引入了新的语法和API(如Integer.getInteger()
和Map.of()
),使得在某些情况下可以避免不必要的装箱操作。
Integer a1 = 128 与 Integer b1 = 128是否相等?
在Java中,当使用==
运算符比较两个Integer
对象i
和j
时,实际上是在比较两个对象的引用是否相同,即是否指向内存中的同一个位置。
对于Integer
类,在Java中有一个特殊的缓存机制。Java虚拟机(JVM)会自动缓存从-128
到127
范围内的Integer
对象。这意味着在这个范围内的Integer
值,如果通过直接赋值的方式创建对象,实际上是指向相同的对象引用。
由于128
不在-128
到127
的范围内,所以当执行Integer i = 128;
和Integer j = 128;
时,即使值相同,i
和j
也会被认为是两个不同的对象,因为它们不在自动装箱的缓存范围内。
因此,i == j
将会返回false
,因为i
和j
指向的是两个不同的Integer
对象实例。
如果想要比较两个Integer
对象的值是否相等,应该使用equals()
方法,如下所示:
Integer i = 128;
Integer j = 128;
boolean areEqual = i.equals(j); // 返回 true,因为比较的是值
八、静态变量与实例变量的区别
在 Java 中,变量可以分为两类:静态变量(也称为类变量)和实例变量。它们之间的主要区别在于作用域、生命周期和访问方式。
静态变量(Static Variables)
-
作用域:静态变量属于类本身,而不是类的某个特定实例。这意味着无论创建多少个类的实例,静态变量在所有实例之间共享。
-
生命周期:静态变量的生命周期与类的加载和卸载相关联。当类被加载时,静态变量会被初始化,当类被卸载时,静态变量会被销毁。
-
访问方式:静态变量可以通过类名直接访问,而不需要创建类的实例。例如:
ClassName.staticVariable
。 -
初始化:静态变量在第一次被访问或程序明确调用其初始化代码时初始化。
-
使用场景:静态变量常用于表示不依赖于类实例的状态,如配置参数、统计信息等。
实例变量(Instance Variables)
-
作用域:实例变量属于类的一个特定实例。每个实例都有自己的实例变量副本,互不影响。
-
生命周期:实例变量的生命周期与对象的生命周期相关。当对象被创建时,实例变量被初始化,当对象不再被引用并被垃圾回收时,实例变量被销毁。
-
访问方式:实例变量只能通过对象的引用访问。例如:
objectName.instanceVariable
。 -
初始化:实例变量在对象创建时初始化,通常是在构造函数中。
-
使用场景:实例变量用于存储每个对象特有的状态信息,如对象的属性。
示例
public class MyClass {
// 静态变量
static int staticVariable;
// 实例变量
int instanceVariable;
// 静态变量的初始化
static {
staticVariable = 10;
}
// 实例变量的初始化可以在构造函数中
public MyClass() {
instanceVariable = 20;
}
}
public class Test {
public static void main(String[] args) {
// 访问静态变量
System.out.println(MyClass.staticVariable); // 输出 10
// 创建实例并访问实例变量
MyClass myObject = new MyClass();
System.out.println(myObject.instanceVariable); // 输出 20
}
}
在这个例子中,staticVariable
是一个静态变量,它在 MyClass
类加载时初始化,并在所有 MyClass
实例之间共享。instanceVariable
是一个实例变量,每个 MyClass
的实例都有自己的 instanceVariable
副本。
总结
理解静态变量和实例变量的区别对于正确设计和使用类非常重要。静态变量适用于存储类级别的共享状态,而实例变量适用于存储对象级别的状态。
九、String s = “” 与 new String() 的区别
在 Java 中,字符串的创建可以通过两种不同的方式进行,它们在内存使用和性能方面有着不同的影响:
String s = ""
这种方式是创建字符串字面量。当使用双引号直接指定一个字符串(如""
),Java 编译器会将其视为一个字符串字面量,并在字符串常量池(String Constant Pool)中寻找是否存在相同的字符串值。
- 如果在字符串常量池中找到了相同的字符串,则会重用该字符串,不会创建新的实例。
- 如果没有找到,编译器会将这个字符串字面量放入字符串常量池中,并创建一个新的
String
对象。
字符串常量池是位于堆内存中的一个特殊的存储区域,用于存储字符串字面量,以节省内存空间。
示例
String s1 = "";
String s2 = "";
System.out.println(s1 == s2); // 输出 true,因为它们引用了相同的对象
new String()
这种方式是通过new
关键字调用String
类的构造函数来创建一个新的String
对象。
- 每次使用
new String()
创建字符串时,无论字符串的值是什么,都会在堆内存中创建一个新的String
对象。 - 这意味着即使两个
new String()
创建的字符串具有相同的值,它们也会是两个完全不同的对象。
示例
String s1 = new String("");
String s2 = new String("");
System.out.println(s1 == s2); // 输出 false,因为它们是两个不同的对象
String str=“abc”和String str=new String(“abc”)会产生几个对象?
-
String str = "abc"
在字符串常量池中创建一个 String 对象,该对象包含值 “abc”。由于字符串常量池旨在优化相同字符串字面量的存储,所以所有相同的字符串字面量都将共享同一个 String 对象。因此,在这种情况下,无论代码中声明多少次 String str = “abc”;,通常只会产生一个 String 对象。 -
String str = new String("abc")
每次执行此语句时,都会在 Java 堆内存中创建一个新的 String 对象。即使字符串的值与字符串常量池中的一个值相同,new String() 也会生成一个新的对象。因此,如果有多行 String str = new String(“abc”);,每一行都会创建一个新的 String 对象。 -
String.intern()
这种行为的一个例外是在 Java 7 及以后的版本中,String.intern()
方法的行为有所改变。intern() 方法用于将一个 String 对象引用到字符串常量池中。如果字符串常量池中已经存在该字符串,则 intern() 会返回常量池中的引用;否则,它会将调用 intern() 的字符串对象添加到常量池中,并返回这个新对象的引用。
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // true,因为它们引用常量池中的同一个对象
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s3 == s4); // false,因为每次都会创建一个新的对象
String s5 = "abc".intern();
String s6 = "abc".intern();
System.out.println(s5 == s6); // true,因为它们都是常量池中的同一个对象
性能和内存影响
- 使用字符串字面量(
""
)通常更节省内存,因为它们可以被存储在字符串常量池中。 - 使用
new String()
创建的字符串可能会占用更多的内存,因为每个String
对象都是独立的实例。 - 在性能方面,字符串字面量由于可能的重用,可能会更快。而
new String()
每次都会执行构造函数,可能会稍慢。
注意
- 字符串常量池的大小可以通过
-XX:StringTableSize
JVM 参数进行调整。 - 从 Java 7 开始,字符串常量池被移动到了堆内存中,而不是永久代(PermGen)。
- 在 Java 8 及以后的版本中,字符串常量池的行为可能受到垃圾回收器的影响,因此在使用时应考虑垃圾回收的策略。
总结
在编写代码时,应根据具体需求选择合适的字符串创建方式。如果需要频繁地创建相同的字符串,使用字符串字面量可能更合适;如果需要动态地创建具有不同值的字符串,使用 new String()
可能更灵活。
十、Switch能否用String做参数
在 Java 中,switch
语句传统上只能使用有限的数据类型作为表达式,主要包括:
byte
short
(或被自动提升为 int 的 short)char
int
枚举(enum)
类型
这意味着在 Java 7 之前,switch
语句不能直接使用 String
作为参数。如果需要根据字符串的值来执行不同的代码分支,通常会使用 if-else
语句。
不过,从 Java 7 开始,switch
语句得到了增强,支持了字符串和枚举类型的使用。因此,在 Java 7 或更高版本中,就可以使用 String
作为 switch
语句的参数。这是一个 Java 7 引入的功能,称为“字符串开关”。
示例
String type = "admin";
switch (type) {
case "admin":
// 管理员相关的代码
break;
case "user":
// 普通用户相关的代码
break;
case "guest":
// 访客相关的代码
break;
default:
// 默认情况下的代码
break;
}
在这个例子中,switch
语句根据 type
字符串的值来决定执行哪个代码块。每个 case
后面紧跟的是 type
可能的值之一,当 switch
表达式的值与 case
标签中的值匹配时,程序将执行该 case
之后的代码,直到遇到 break
语句。
注意事项
- 每个
case
标签中的字符串值必须是唯一的,不能重复。 - 如果使用了
switch
语句处理字符串,建议包含一个default
分支,以处理未明确列出的任何其他字符串值。 - 在 Java 7 之前的版本中,不能使用
String
作为switch
的参数,只能使用if-else
语句或者使用其他支持的数据类型。
增强后的 switch
语句使得处理基于字符串的条件逻辑变得更加清晰和方便。
十一、什么是字节码?采用字节码的最大好处是什么?
字节码是 Java 程序编译后的中间代码,是⼀种可移植的⼆进制代码,可以在任何⽀持 Java虚拟机的平台上运⾏。字节码通过将 Java 源代码编译为字节码指令序列,使得 Java程序可以跨平台运⾏,即使是在不同的操作系统和硬件平台上也可以运⾏。
字节码采⽤中间代码的形式,相⽐于直接将程序编译为特定平台上的机器码,有以下⼏个好处:
-
可移植性:由于字节码是中间代码,所以可以在任何⽀持 JVM 的平台上运⾏,使得 Java程序具有很好的可移植性。这也是 Java 跨平台的重要特性之⼀。
-
安全性:由于字节码需要在 JVM 中运⾏,所以可以对字节码进⾏安全检查,以确保程序不会对系统造成威胁。
-
性能:由于字节码是⼀种紧凑的⼆进制格式,相⽐于直接编译为机器码,可以更快地加载和传输,同时也可以在运⾏时进⾏动态优化,提⾼程序的执⾏效率。
-
可读性:相⽐于直接编译为机器码,字节码具有更好的可读性,可以⽅便地进⾏反汇编和调试。
十二、Java 中 final 关键字有什么⽤?
在Java中,final
关键字可以用于类、方法和变量,具有不同的含义和用途:
-
final修饰变量:当一个变量被声明为
final
,它意味着这个变量一旦被初始化赋值后,就不能再被重新赋值。final
变量可以是类的成员变量、局部变量或方法参数。对于基本数据类型,final
意味着其值不可变;对于对象引用,final
意味着引用不可变,但对象本身是可以被修改的(除非对象本身也是不可变的)。 -
final修饰方法:一个声明为
final
的方法不能被子类重写。这可以确保该方法的行为不会被改变,是类的固定行为。使用final
方法可以防止类的继承者改变方法的实现,有助于保护类的完整性。 -
final修饰类:当一个类被声明为
final
,这意味着该类不能被其他类继承。使用final
类可以确保类的实现不会被修改,这在创建不可变类或工具类时非常有用。 -
匿名内部类中的实例初始化:在匿名内部类中,如果构造器中的参数被声明为
final
,那么这些参数可以在匿名内部类的实例方法中安全地使用,因为它们的值在对象构造时就已经确定,并且在对象的整个生命周期内不会改变。 -
lambda表达式中的参数:在Java 8及以后的版本中,lambda表达式的参数隐式地被视为
final
,这意味着它们在lambda表达式的作用域内是不可变的。
使用final
关键字的好处包括:
- 不变性:
final
变量提供了不变性,有助于减少错误和提高代码的可预测性。 - 线程安全:由于
final
变量的值不可变,它们自然地是线程安全的,无需额外的同步措施。 - 设计清晰:
final
方法和类使得类的接口更加清晰,因为它们不能被改变或扩展。 - 性能优化:编译器和JVM可以对
final
方法和类进行优化,因为它们的行为是确定的。
总之,final
关键字是Java编程中一个非常重要的工具,它有助于提高代码的安全性、清晰性和性能。
十三、HashCode与equals的区别?什么是Hash碰撞?
HashCode和equals方法都是Objects类的方法。两者的区别如下:
-
hashCode()
是一个方法,它返回对象的哈希码,即一个整数值。哈希码用于在哈希表中确定对象存储的位置。equals()
是一个方法,用于比较两个对象是否相等。equals()
默认实现是检查对象的内存地址是否相同,但通常需要重写此方法以提供逻辑上的相等性比较(例如,比较对象的属性值)。 -
根据Java的规定,
hashCode()
应该在equals()
方法被重写时同时被重写,以保证相等的对象有相同的哈希码。而如果两个对象通过equals()
方法比较结果为true
,那么它们的hashCode()
也应该相同。 -
hashCode()
⽅法返回的是⼀个 int 类型的数。equals()
⽅法返回的是⼀个 boolean 类型的值。
哈希碰撞(Hash Collision):
哈希碰撞是指两个或多个不同的对象拥有相同的哈希码值。由于哈希函数将对象映射到有限数量的哈希桶中,不同的对象可能会映射到同一个哈希桶,这就是所谓的碰撞。
哈希碰撞是不可避免的,因为哈希码的值域(通常是整数)是有限的,而对象的数量可能是无限的。但是,可以通过以下方式来减少碰撞的发生:
- 使用好的哈希函数:一个好的哈希函数能够均匀地分布对象,减少碰撞的可能性。
- 使用开放寻址法:当发生碰撞时,可以通过探测序列找到下一个空闲的哈希桶。
- 使用链地址法:每个哈希桶可以包含一个链表,所有映射到该桶的对象都存储在这个链表中。
在Java的HashMap
中,如果两个对象的hashCode()
相同,它们将被存储在同一个哈希桶中,但是通过链表或树(当链表过长时)来解决碰撞问题。
十四、为什么需要同时重写hashCode()和equals()?
- 一致性:如果两个对象通过
equals()
方法比较结果为true
,那么它们的hashCode()
也应该相同,以保证它们在哈希表中的一致性。 - 性能:如果
hashCode()
没有被正确重写,可能会导致哈希表的性能下降,因为很多对象可能会映射到同一个哈希桶,从而增加查找时间。
总结来说,hashCode()
和equals()
是Java中用于对象比较和哈希表操作的两个重要方法,它们需要协同工作以确保对象的正确比较和哈希表的有效性。
十五、String 和 StringBuffer、StringBuilder 的区别是什么?
String
、StringBuffer
和StringBuilder
是Java中常用的三个类,它们都用于处理字符串,但它们之间存在一些重要的区别:
-
String:
String
是一个不可变对象,一旦创建就不能更改。- 每次对
String
对象进行修改操作时,实际上都会生成一个新的String
对象。 String
适合用于不需要频繁修改字符串的场景。
-
StringBuffer:
StringBuffer
是一个可变的字符串缓冲区,可以在其内容上进行修改。StringBuffer
是线程安全的,即它的实例可以在多线程环境下使用而不需要额外的同步措施。- 由于
StringBuffer
是线程安全的,因此在执行字符串操作时可能会有一些性能开销。
-
StringBuilder:
StringBuilder
与StringBuffer
类似,也是一个可变的字符串缓冲区,可以进行修改。StringBuilder
不是线程安全的,因此它在单线程环境下的性能比StringBuffer
更好。- 由于
StringBuilder
不是线程安全的,所以在多线程环境下使用时需要额外的同步措施。
从性能考虑:
- 当需要频繁修改字符串时,使用
StringBuffer
或StringBuilder
比使用String
更高效,因为String
的不可变性会导致每次修改都创建一个新的对象。 - 在单线程环境下,推荐使用
StringBuilder
,因为它避免了StringBuffer
的线程同步开销。 - 在多线程环境下,如果需要线程安全,应该使用
StringBuffer
。
从线程安全性考虑:
String
是不可变的,因此自然是线程安全的。StringBuffer
提供了线程安全保障,适合在多线程环境中使用。StringBuilder
不是线程安全的,但在单线程环境中可以提供更好的性能。
示例:
String str = "Hello";
// 实际上创建了一个新的String对象
str += " World";
StringBuffer sb = new StringBuffer("Hello");
// 在原有对象上修改
sb.append(" World");
StringBuilder sb = new StringBuilder("Hello");
// 在原有对象上修改,性能优于StringBuffer
sb.append(" World");
十六、什么是 Java 内部类? 内部类的分类有哪些 ?内部类有哪些优点和应⽤场景?
Java内部类是指定义在另一个类的内部的类,它与外部类(即包含内部类的类)存在一种特殊的关系。内部类可以访问外部类的成员,包括私有成员,而不需要特别的权限。
Java内部类的分类主要有以下几种:
- 成员内部类:定义在外部类的成员位置上,即在外部类的其他成员变量或方法之间。
- 局部内部类:定义在一个方法内部的类,通常用于实现特定的功能。
- 匿名内部类:没有名字的内部类,通常用于继承其他类或实现接口,并且只使用一次。
- 静态内部类:定义在外部类内部,但是使用static关键字声明,它不持有对外部类实例的引用。
内部类的优点:
- 封装性:内部类可以对外部类进行更好的封装,隐藏实现细节。
- 访问控制:内部类可以访问外部类的私有成员,而外部类不能访问内部类的私有成员。
- 代码组织:内部类可以使得代码更加模块化,有助于逻辑的清晰和代码的重用。
- 继承和多态:内部类可以继承其他类或实现接口,支持多态。
- 匿名内部类:可以简化代码,特别是在需要一次性使用某个类时。
应用场景:
- 实现回调:使用匿名内部类可以实现回调机制,如事件处理。
- 构建代理:内部类可以用于实现代理模式,如保护代理、远程代理等。
- 实现事件监听器:在Java的事件处理模型中,经常使用内部类或匿名内部类来实现事件监听器。
- 实现线程:内部类可以用于创建线程,特别是当线程需要访问外部类的私有成员时。
- 组织大型项目:在大型项目中,内部类可以用于组织和封装相关的类,使项目结构更加清晰。
示例:
public class OuterClass {
private int outerVar = 0;
// 成员内部类
class InnerClass {
public void printOuterVar() {
System.out.println(outerVar);
}
}
// 局部内部类
public void method() {
class LocalInnerClass {
public void display() {
System.out.println("Local Inner Class");
}
}
LocalInnerClass lic = new LocalInnerClass();
lic.display();
}
// 匿名内部类
public void createInstance() {
Runnable runnable = new Runnable() {
public void run() {
System.out.println("Running in a thread");
}
};
new Thread(runnable).start();
}
// 静态内部类
static class StaticInnerClass {
public void display() {
System.out.println("Static Inner Class");
}
}
}
十七、抽象类与普通类的区别?
抽象类(Abstract Class)与普通类在Java中有一些关键的区别:
-
抽象方法:
- 抽象类可以包含抽象方法,即没有实现的方法,只有声明。
- 普通类不能包含抽象方法。
-
实例化:
- 抽象类不能被实例化,即不能创建抽象类的对象。
- 普通类可以被实例化,可以创建具体对象。
-
子类:
- 抽象类可以被其他类继承,并要求子类提供抽象方法的具体实现(除非子类也是抽象类)。
- 普通类也可以被继承,但子类可以选择重写或不重写父类的方法。
-
构造方法:
- 抽象类可以有构造方法,但这些构造方法不能直接调用,只能通过子类构造方法的super调用。
- 普通类也可以有构造方法,可以直接通过new关键字创建对象时调用。
-
设计目的:
- 抽象类的主要目的是为其他类提供一个公共的基类,定义一组相关对象的公共属性和方法。
- 普通类通常用于定义具体的数据结构和行为,可以独立存在和使用。
-
使用场景:
- 当你想要为一组具有共性的类定义一个公共的模板时,抽象类是一个很好的选择。
- 当你有一个完整的类,具有具体实现,不需要被其他类继承时,应该使用普通类。
-
关键字:
- 抽象类使用
abstract
关键字声明。 - 普通类不使用
abstract
关键字。
- 抽象类使用
-
接口实现:
- 抽象类可以不实现(或部分实现)接口中的方法。
- 普通类可以实现一个或多个接口,并提供所有接口方法的具体实现。
示例:
// 抽象类示例
abstract class Animal {
abstract void makeSound(); // 抽象方法
// 可以有具体的方法
void eat() {
System.out.println("Eating");
}
}
// 普通类示例
class Dog extends Animal {
public void makeSound() {
System.out.println("Bark");
}
}
// 抽象类不能被实例化
Animal myAnimal = new Animal(); // 编译错误
// 但可以创建抽象类的子类的实例
Dog myDog = new Dog();
myDog.makeSound(); // 输出 "Bark"
总结来说,抽象类是一种特殊的类,它不能被实例化,并且可以包含抽象方法,主要用于定义一个共同的模板供其他类继承。普通类是完整的类,可以被实例化,并且包含具体实现的方法。
十八、接口和抽象类有什么区别?
接口(Interface)和抽象类(Abstract Class)都是面向对象编程中用于抽象化概念的工具,但它们之间存在一些重要的区别:
-
抽象程度:
- 抽象类可以包含抽象方法和具体方法,它可以有具体实现的部分。
- 接口只能包含抽象方法(Java 8之前),以及默认方法和静态方法(Java 8及之后),但不能有具体实现。
-
实现方式:
- 一个类可以实现多个接口,但只能继承一个抽象类。
- 接口的实现是通过关键字
implements
,而抽象类的继承是通过关键字extends
。
-
构造方法和字段:
- 抽象类可以有构造方法和字段,并且字段可以是各种类型的(静态或非静态)。
- 接口在Java 8之前不能有构造方法和字段,从Java 8开始,接口可以有静态常量(默认是
public
和static
的),以及默认方法和静态方法。
-
访问修饰符:
- 抽象类中的成员可以有各种访问修饰符,如
public
、protected
、private
。 - 接口中的字段默认是
public
和static
的,从Java 8开始,接口方法可以有default
关键字,表示有默认实现。
- 抽象类中的成员可以有各种访问修饰符,如
-
主要目的:
- 抽象类的目的是提供一个共同的基类,可以包含一些共同的行为和特征。
- 接口的目的是定义一个协议或规范,规定实现类必须遵守的规则。
-
多态性:
- 抽象类和接口都支持多态性,但接口提供了一种不同的多态性形式,即所有的方法都是抽象的,实现类必须实现这些方法。
-
使用场景:
- 当你需要定义一个类模板,并且这个类有一些具体实现时,应该使用抽象类。
- 当你需要定义一个对象的行为协议,并且这个协议可以有多种不同的实现时,应该使用接口。
-
版本更新:
- 从Java 9开始,接口可以包含私有方法和私有静态方法,这进一步增强了接口的功能性。
示例:
// 抽象类示例
abstract class Animal {
public abstract void makeSound();
public void eat() {
System.out.println("Animal is eating.");
}
}
// 接口示例
interface Drivable {
void drive();
}
interface Parkable {
void park();
}
class Car implements Drivable, Parkable {
public void drive() {
System.out.println("Car is driving.");
}
public void park() {
System.out.println("Car is parked.");
}
}
在这个示例中,Animal
是一个抽象类,它有一个抽象方法makeSound()
和一个具体方法eat()
。Car
类实现了Drivable
和Parkable
两个接口,并且提供了这两个接口中所有方法的具体实现。
总结来说,抽象类提供了一个模板,可以包含具体实现,而接口定义了一个协议,只包含抽象方法和一些默认方法。选择使用抽象类还是接口取决于具体的设计需求和场景。
十九、重载和重写
重载(Overload)和重写(Override)是面向对象编程中的两个重要概念,它们允许在不同的上下文中以相同的名称调用方法。
重载(Overload)
-
定义:重载指的是在同一个类中,有两个或多个方法在类中具有相同的名称,但参数列表不同。参数列表的不同可以是参数的数量不同、参数的类型不同或它们的排列顺序不同。
-
目的:重载允许程序员定义多个具有相同名称但接受不同参数的方法,这使得代码更加清晰和易于理解。
-
规则:
- 方法名必须相同。
- 参数列表必须不同(数量、类型或顺序)。
- 返回类型可以相同也可以不同,但这不影响方法的重载。
- 访问修饰符和异常声明可以不同,但它们不是决定重载的关键因素。
-
编译时处理:编译器在编译时根据方法的签名(方法名和参数列表)来确定调用哪个重载的方法。
重写(Override)
-
定义:重写发生在继承体系中,当子类有一个与父类中具有相同名称、相同参数列表和相同返回类型的方法时,子类可以提供自己的实现来覆盖父类中的方法。
-
目的:重写允许子类改变从父类继承来的方法的行为,这是多态性的一个重要方面。
-
规则:
- 方法名、参数列表和返回类型必须完全相同。
- 子类方法不能有比父类方法更严格的访问权限。
- 子类方法不能抛出比父类方法更广泛的异常。
- 如果父类方法声明为
final
,则不能被重写。
-
运行时处理:Java虚拟机(JVM)在运行时根据对象的实际类型来确定调用哪个重写的方法,这是多态性的一部分。
示例:
class Parent {
void display(int a) {
System.out.println("Parent display with int: " + a);
}
}
class Child extends Parent {
@Override // 这不是必须的,但用于提高代码可读性
void display(int a) {
System.out.println("Child display with int: " + a);
}
// 重载display方法
void display(String a) {
System.out.println("Child display with String: " + a);
}
}
public class Test {
public static void main(String[] args) {
Child c = new Child();
// 调用Child类的display(int a)方法
c.display(10);
// 调用Child类的display(String a)方法
c.display("Hello");
}
}
在这个示例中,display
方法在Child
类中被重写,同时Child
类还提供了一个新的重载版本,接受String
类型的参数。
总结来说,重载是同一个类中具有相同名称但参数不同的方法,而重写是子类中具有与父类完全相同的方法签名的方法,用于改变父类方法的行为。
二十、Java运算符有哪些?
Java提供了一系列的运算符,用于执行各种操作,如数学计算、比较、逻辑操作等。
以下是Java中的一些主要运算符类别及其用途:
1. 算术运算符
用于基本的数学运算:
+
加法-
减法*
乘法/
除法%
取模(求余数)++
递增(自增1)--
递减(自减1)
2. 关系运算符
用于比较两个值,并返回布尔值(true或false):
==
等于!=
不等于>
大于<
小于>=
大于等于<=
小于等于
3. 逻辑运算符
用于布尔逻辑运算:
&&
逻辑与(AND)||
逻辑或(OR)!
逻辑非(NOT)
4. 位运算符
用于对整数的二进制位进行操作:
&
位与|
位或^
位异或~
位非(一元运算符)<<
左移位>>
右移位(算术右移位)>>>
无符号右移位
5. 赋值运算符
用于给变量赋值:
=
简单赋值+=
加后赋值-=
减后赋值*=
乘后赋值/=
除后赋值%=
取模后赋值&=
位与后赋值|=
位或后赋值^=
位异或后赋值<<=
左移位后赋值>>=
右移位后赋值>>>=
无符号右移位后赋值
6. 三元运算符
用于基于条件进行选择:
? :
三元条件运算符,格式为condition ? value_if_true : value_if_false
7. 扩展赋值运算符(Java 8引入)
用于链式调用赋值:
*=
,/=
,%=
,+=
,-=
,<<=
,>>=
,>>>=
,&=
,^=
,|=
8. 其他运算符
.
成员访问(点运算符)[]
数组索引访问()
强制类型转换或方法调用new
创建对象或数组实例
示例:
int a = 10;
int b = 20;
// 算术运算
int sum = a + b;
int difference = a - b;
int product = a * b;
double quotient = (double) a / b; // 强制类型转换为double
int remainder = a % b;
// 关系运算
boolean isLess = a < b;
// 逻辑运算
boolean isTrue = (a > b) && (a != 0);
// 位运算
int bitwiseAnd = a & b;
int bitwiseOr = a | b;
int bitwiseXor = a ^ b;
// 赋值运算
a += 5; // 等同于 a = a + 5;
// 三元运算符
int max = (a > b) ? a : b;
// 扩展赋值运算符(Java 8+)
List<String> list = new ArrayList<>();
list.add("Hello");
list.addAll(Arrays.asList("World", "Java")); // 链式调用
二一、JDK、JRE、JVM有什么区别?
-
JDK
是Java开发工具包,包含了编写、编译、调试和运行Java程序所需的所有工具和组件,比如编译器(Javac)、Java APi、调试工具等。JDK是针对开发人员的。 -
JRE
是Java运行时环境,包括了Java虚拟机(JVM)和Java标准类库(Java API)。JRE是针对Java应用程序的,它提供了在计算机上运行Java应用程序所需的最小环境。 -
JVM
是Java虚拟机,是Java程序运行的环境。JVM负责将Java代码解释或编译为本地机器代码,并在运行时提供必要的环境支持,比如内存管理、垃圾回收、安全性等。JVM的主要作用是将Java代码转换为可以再计算机上运行的机器码,并负责程序的执行。
二二、OOP和AOP的区别?
-
面向对象编程OOP: 面向对象编程是一种编程范式,它使用“对象”来设计软件。对象可以包含数据(通常称为属性或字段)和代码(通常称为方法或函数)。OOP的核心概念包括封装、继承、多态和抽象。
-
目的: OOP的目的是提高代码的可重用性、灵活性和可维护性,通过创建模块化和易于管理的代码结构。
-
实现:在OOP中,开发者创建类和对象来构建应用程序。类是对象的蓝图,对象是类的实例。
-
关注点:OOP关注于数据和行为的封装,以及它们之间的关系。
-
应用:OOP广泛应用于软件开发,几乎所有的编程语言都支持OOP。
-
语言:几乎所有的编程语言,如Java、C++、Python、Ruby等,都支持OOP。
-
-
面向切面编程AOP: 面向切面编程是一种编程范式,它允许开发者将横切关注点(如日志记录、事务管理、安全性等)与业务逻辑分离。AOP通过使用“切面”来实现这一点,切面可以插入到应用程序的特定连接点(如方法的调用)。
-
目的: AOP的目的是解决软件系统中的横切关注点问题,这些关注点通常与业务逻辑无关,但需要在多个地方重复实现。
-
实现:在AOP中,开发者定义切面和通知(Advice),这些通知可以在应用程序的特定连接点上执行,例如在方法调用之前、之后或周围。
-
关注点:AOP关注于将系统的不同部分(如日志记录、安全性、事务管理等)与核心业务逻辑分离。
-
应用:AOP通常与OOP结合使用,以解决OOP难以处理的横切关注点问题。它在Java Spring框架等中得到了广泛应用。
-
语言:AOP通常需要特定的工具或框架来实现,如AspectJ(Java)、PostSharp(.NET)等。
-
总结来说,OOP是一种更为普遍的编程范式,它注于数据和行为的封装,而AOP是一种补充性的编程范式,关注于将横切关注点与业务逻辑分离。
二三、介绍下AOP的核心概念
AOP的核心概念包括:
-
切面(Aspect):切面是AOP中的核心概念,它定义了一组横切关注点。一个切面可以包含多个通知(Advice)和切点(Pointcut)。
-
连接点(Join Point):连接点是指程序执行过程中可以插入切面的地方。在Java中,方法的调用、异常的抛出、字段的访问等都可以作为连接点。
-
切点(Pointcut):切点用于定义哪些连接点将被切面所影响。它是一个表达式,用于匹配连接点。
-
通知(Advice):通知是切面的一部分,它定义了在切点处要执行的动作。通知可以是前置(Before)、后置(After)、返回(After returning)、异常(After throwing)或环绕(Around)。
-
织入(Weaving):织入是将切面应用到目标对象并创建一个被增强的对象的过程。织入可以在编译时(编译时织入)、类加载时(加载时织入)或运行时(动态代理)进行。
-
目标对象(Target Object):目标对象是被切面所增强的对象。
-
代理(Proxy):在运行时织入时,AOP框架通常使用代理模式。代理对象在调用目标对象的方法之前或之后执行切面中定义的逻辑。
-
引入(Introduction):引入允许AOP框架为类添加新的方法或属性。
AOP的主要优点是提高了代码的模块化和可维护性,因为它允许开发者将与业务逻辑无关的横切关注点分离出来,使得业务逻辑更加清晰和易于理解。AOP在企业级应用程序开发中非常有用,尤其是在需要处理复杂事务、安全性、日志记录等场景时。
二四、什么叫序列化,怎么去序列化?
1、 序列化:是一种用来处理对象流的机制,所谓对象流也就是将对象流化,使对象在一定的介质(读写操作、文件传输)字节化。序列化是为了解决在对对象流进行读写操作时所引发的问题。
Java序列化的实现
- 实现Serializable接口:要使一个Java对象是可序列化的,它必须实现java.io.Serializable接口。这个接口是一个标记接口,不包含任何方法。 (对于不想进行序列化的变量,可以使用transient关键字修饰。)
- 使用ObjectOutputStream:使用Java的ObjectOutputStream类来将对象写入到输出流中。
- 处理序列化版本号:为了防止序列化版本冲突,Java建议为每个可序列化的类定义一个serialVersionUID字段。
2、反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
反序列化过程:
- 实现Serializable接口:和序列化一样,反序列化的对象也必须实现Serializable接口。
- 使用ObjectInputStream:使用Java的ObjectInputStream类来读取序列化后的对象。
- 处理ClassNotFoundException:在反序列化过程中,如果找不到类定义,会抛出ClassNotFoundException。
对象的序列化主要有两种用途:
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
- 在网络上传送对象的字节序列。
Java序列化的例子:
import java.io.*;
public class Person implements Serializable {
private static final long serialVersionUID = 1L; // 版本号
private String name;
private int age;
// 构造函数、getter和setter省略
}
public class SerializationDemo {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
out.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"))) {
Person deserializedPerson = (Person) in.readObject();
System.out.println("Deserialized Person: " + deserializedPerson.getName() + ", " + deserializedPerson.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
注意事项:
- 安全性:序列化机制可以被用来执行安全攻击,如序列化炸弹(通过发送大量数据导致内存溢出)。
- 性能:序列化和反序列化过程可能会比较慢,特别是对于大型对象。
- 兼容性:在序列化和反序列化过程中,类的版本号(serialVersionUID)必须匹配,否则会抛出InvalidClassException。
Java的序列化机制是Java语言中处理对象持久化和网络传输对象的标准方式之一。然而,由于其性能和安全性问题,现代Java应用程序中通常会考虑使用其他序列化框架,如Google的Protocol Buffers、Apache的Thrift或JSON等。
Java 容器篇
一、请你说一下集合体系,从顶层说起
Java集合框架(Java Collections Framework,简称JCF)是一个用于存储和处理对象集合的丰富而强大的工具箱。集合框架的设计非常灵活,它允许用户以各种方式对数据进行操作。以下是Java集合体系的顶层结构和主要组成部分:
单列值集合Collection
-
根接口
java.util.Collection
:集合框架的顶层接口,定义了集合的基本操作,如添加、删除元素等。Collection
有两个主要的子接口:List
和Set
。 -
List
接口:表示有序的元素集合,可以包含重复元素。允许元素的重复并维护元素的插入顺序。主要实现类包括:ArrayList
:基于动态数组实现,适合随机访问。不是线程安全的。Vector
:和ArrayList类似,但它是线程安全的,但通常不如ArrayList性能好。LinkedList
:基于双向链表实现,适合频繁的插入和删除操作。
-
Set
接口:表示无序的(由实现类决定)元素集合,不包含重复元素。主要实现类包括:HashSet
:基于哈希表实现,不保证元素的迭代顺序。TreeSet
:基于红黑树实现,可以保持元素的自然排序或根据提供的Comparator进行排序。LinkedHashSet
:类似于HashSet,但维护了元素的插入顺序。
-
Queue
接口:是Collection
的子接口,用于处理元素的排列,通常按照特定的顺序(如先进先出FIFO)。典型实现类:LinkedList
(作为队列使用),PriorityQueue
。 -
Deque
接口:是Queue
的子接口,表示双端队列,允许在两端插入和移除元素。典型实现类:ArrayDeque
,LinkedList
(也可以作为双端队列使用)。
键值对双列集合Map
Map
接口:表示键值对的集合,它根据键对元素进行映射,并且每个键最多只能映射到一个值。Map
不是Collection
的子接口,但它是集合框架的另一个基础部分。主要实现类包括:HashMap
:基于哈希表实现,不保证映射的顺序。Hashtable
:和HashMap类似,但它是线程安全的,并且是同步的。TreeMap
:基于红黑树实现,可以保持键的自然排序或根据提供的Comparator进行排序。LinkedHashMap
:保留了插入顺序,同时也可以根据访问顺序改变元素的位置。
工具类
Collections
:提供了大量静态方法来操作或返回集合,如sort(), shuffle(), fill()等。Arrays
:提供了操作数组和列表的方法,如sort(), binarySearch(), asList()等。
迭代器(Iterator):用于遍历集合中的元素,提供了一种方法来安全地遍历集合。
泛型(Generics):允许在编译时提供类型安全,可以创建操作特定类型对象的集合。
并发集合:线程安全的集合,用于多线程环境下的集合操作。
- ConcurrentHashMap
:线程安全的哈希表映射。
- CopyOnWriteArrayList
:线程安全的变体,适用于读多写少的场景。
不可变集合:不可变的集合类,如Collections.unmodifiableList()
等,提供了对原始集合的只读视图。
集合算法:集合框架提供了一些静态方法,如Collections.sort()
, Collections.max()
, Collections.min()
等,用于对集合进行操作。
Java集合框架的设计非常灵活,它允许开发者根据需要选择不同的集合类型和操作。通过合理选择和使用集合类型,可以提高程序的性能和可维护性。
二、如何选用集合
- 如果需要快速访问列表中的元素,使用ArrayList。
- 如果需要在列表中间进行插入或删除,使用LinkedList。
- 如果需要一个不允许重复的集合,使用HashSet。
- 如果需要保持插入顺序并且需要快速查找,使用LinkedHashSet。
- 如果需要根据元素的自然顺序或自定义顺序对集合进行排序,使用TreeSet。
- 如果需要存储键值对,使用HashMap或TreeMap。
- 如果需要线程安全的集合,使用ConcurrentHashMap或Collections.synchronizedList等线程安全的包装类。
三、LinkedList和ArrayList的区别
-
内部数据结构:
ArrayList
:基于动态数组(即数组)实现,可以看作是能够自动调整大小的数组。LinkedList
:基于双向链表实现,每个元素都是链表的一个节点。
-
性能特点:
ArrayList
:- 随机访问(通过索引定位元素)非常快速,时间复杂度为O(1)。
- 增加和删除元素时,会对操作点之后的所有数据下标索引造成影响,需要进行扩容和复制操作,时间复杂度为O(n)。
LinkedList
:- 随机访问相对较慢,因为需要从头开始遍历链表,时间复杂度为O(n)。
- 在任意位置增加和删除元素非常快速,因为只需要改变节点的指针,时间复杂度为O(1)。
-
内存使用:
ArrayList
:由于数组的连续性,内存使用相对更紧凑。LinkedList
:每个元素都需要额外的存储空间来维护指向前一个和后一个元素的指针,因此内存使用上不如ArrayList
紧凑。
-
** 扩容**:
ArrayList
有一个固定的扩容因子,当超过当前容量时会按因子增长。LinkedList
不需要扩容,因为它不是基于固定大小的数组。
- 其他操作:
LinkedList
可以作为一个栈(Stack)、队列(Queue)或双端队列(Deque)使用,因为链表可以在两端快速地添加或移除元素。
选择使用ArrayList
还是LinkedList
应基于程序的具体需求和操作特性。例如,如果程序中包含大量的搜索操作,ArrayList
可能是更好的选择;而如果程序需要频繁地在列表中插入和删除元素,LinkedList
可能更为合适。
四、HashMap和HashTable的区别,哪个的性能更好?
HashMap
和Hashtable
都是Java中实现Map接口的两个不同的类,它们在设计和性能上有一些显著的区别:
-
线程安全性:
HashMap
是非线程安全的,这意味着在没有额外同步措施的情况下,它不适合在多线程环境中使用。Hashtable
是线程安全的,它的所有方法都通过synchronized
关键字进行了同步,这使得它在多线程环境中可以安全使用,但会降低性能。
-
性能:
HashMap
是非线程安全的,不支持多线程操作。如果需要在多线程环境中使用,可以通过Collections.synchronizedMap
方法来实现同步。HashTable
是线程安全的,内部的方法都是Synchronized
同步的,可以直接在多线程环境中使用。
-
迭代器:
HashMap
的迭代器是fail-fast迭代器,不允许在迭代过程中对Map进行结构性修改,否则会抛出ConcurrentModificationException异常。HashTable
的Enumeration不是fail-fast的,允许在迭代中进行结构性修改。
-
对
null
的处理:HashMap
允许一个null
键(key)和多个null
值(value)。Hashtable
不允许键或值是null
,如果尝试插入null
键或值,将会抛出NullPointerException
。
-
初始容量和扩容:
HashMap
的默认初始容量是16,而Hashtable
的默认初始容量是11。- 当需要扩容时,
HashMap
的容量通常会翻倍,而Hashtable
的容量是翻倍再加1,这有助于减少因哈希冲突导致的链表过长问题。
-
哈希算法:
HashMap
在计算哈希值时使用了额外的哈希算法来减少哈希冲突。Hashtable
直接使用对象的hashCode()
方法计算哈希值,并且通过取模运算来确定哈希桶的索引。
-
继承体系:
HashMap
继承自AbstractMap
,而Hashtable
继承自Dictionary
类,后者是一个已经被废弃的类。
-
遍历方式:
HashMap
的迭代器是快速失败的(fail-fast),这意味着在迭代过程中如果检测到映射结构被修改,迭代器会立即抛出ConcurrentModificationException
。Hashtable
的迭代器不是快速失败的,它使用的是Enumerator
,这在某些情况下可能不会抛出异常。
-
推荐使用:
- 在单线程环境下,如果没有特殊的线程安全需求,推荐使用
HashMap
,因为它提供了更好的性能。 - 如果需要线程安全的Map实现,推荐使用
ConcurrentHashMap
,它在保持线程安全的同时,通过分段锁减少了性能损耗。
- 在单线程环境下,如果没有特殊的线程安全需求,推荐使用
综上所述,HashMap
在单线程环境下性能更好,因为它避免了同步带来的开销。然而,如果应用程序需要线程安全的Map,应该考虑使用ConcurrentHashMap
而不是Hashtable
。
五、HashSet和TreeSet的特征
HashSet和TreeSet是Java中的两种Set集合,它们的特征可以简要描述如下:
HashSet
是基于哈希表实现的,无序的集合,它使用哈希函数来存储元素,因此添加、删除和查找元素的操作都很高效。
HashSet
不会保留元素的插入顺序,且不允许存储重复的元素,如果添加重复元素,集合不会进行任何改变。
TreeSet
是基于红黑树实现的,它是有序的集合,根据元素的自然顺序或者提供的比较器对元素进行排序。
TreeSet
会对元素进行排序,使得元素按升序或者自定义顺序排列,且不允许存储重复的元素。
总的来说,HashSet适合对集合进行快速操作且不关心元素顺序的场景,而TreeSet适合对集合元素进行排序并控制添加的元素不重复的场景。就像是一本杂志和一本字典,一个是无序的,一个是有序的,并且有自己的排列规则。
六、HashSet如何检查重复
HashSet
是 Java 中的一个集合类,它继承自 AbstractSet
并直接实现了 Set
接口。HashSet
能够存储不重复的元素,其检查重复的机制主要依赖于其存储元素的两个核心属性:唯一性和哈希值。
HashSet
检查重复的步骤:
-
哈希函数:
当一个对象被添加到HashSet
中时,它首先会调用该对象的hashCode()
方法来计算对象的哈希值。哈希值是一个整数,它代表了对象的哈希码。 -
哈希表:
HashSet
内部使用哈希表(实际上是一个数组)来存储元素。哈希值决定了对象在哈希表中的索引位置,即对象应该被存储在数组的哪个位置。 -
冲突解决:
如果两个对象有不同的哈希值,它们会被存储在哈希表的不同位置,从而不会发生冲突。但如果两个对象有相同的哈希值(这种情况被称为哈希冲突),HashSet
会使用链表或红黑树(在 JDK 1.8 及以后的版本中,当链表长度超过一定阈值时会转换为红黑树)来解决冲突。 -
equals() 方法:
当两个对象有相同的哈希值并且映射到哈希表的同一个位置时,HashSet
会进一步使用对象的equals()
方法来比较这两个对象是否真正相等。如果equals()
返回true
,则认为这两个对象是相等的,HashSet
会认为它们是重复的,并不会添加重复的对象。 -
添加操作:
如果HashSet
检测到要添加的对象与集合中的某个对象相等(通过equals()
方法比较),则不会添加这个新对象,避免重复。如果所有现有对象都不与之相等,新对象会被添加到HashSet
中。 -
性能考虑:
HashSet
通过哈希表和合理的哈希函数来最小化冲突,并确保添加和查找操作的效率。理想情况下,哈希操作的时间复杂度是 O(1),即使在有大量元素的情况下也能保持高效的性能。
总结来说,HashSet
通过哈希值和 equals()
方法的结合来检查并避免存储重复的元素,确保了集合的唯一性。
七、HashMap 和 ConcurrentHashMap 的区别?
HashMap
和ConcurrentHashMap
是Java中实现Map接口的两个不同的类,它们在多线程环境下的行为和性能有显著的区别:
-
线程安全性:
HashMap
不是线程安全的,如果在多线程环境中对 HashMap 进行并发操作,可能会导致数据不一致甚至抛出ConcurrentModificationException
异常。ConcurrentHashMap
是线程安全的。它通过内部同步机制来保证在多线程环境下的安全性,允许多个线程同时读写而不会出现竞争条件。
-
性能:
- 在单线程环境下,
HashMap
通常提供更好的性能,因为它避免了同步造成的开销。 ConcurrentHashMap
由于其同步机制,可能会在单线程环境下性能稍差。但在多线程环境下,由于其高效的并发控制,通常能提供更好的性能和吞吐量。
- 在单线程环境下,
-
内部结构:
HashMap
从JDK 1.8开始,内部结构由数组、链表和红黑树组成,当桶中的链表长度超过一定阈值(默认为8)时,链表会被转换成红黑树,以提高搜索效率。ConcurrentHashMap
在JDK 1.7中使用分段锁机制,由多个独立的子Map组成,每个子Map独立加锁。在JDK 1.8中,它的内部结构也由数组、链表和红黑树组成,并且采用了更细粒度的锁机制,每个桶单独加锁,进一步提高了并发性能。
-
懒加载:
HashMap
在实例化时可以选择是否初始化底层数组,它采用了一种懒加载策略,直到第一个元素插入时才真正初始化数组。ConcurrentHashMap
在JDK 1.8中也采用了类似的懒加载策略,只有在需要时才会初始化数组和桶。
-
适用场景:
HashMap
适用于单线程环境或读多写少的场景,以及不需要保证线程安全的情况。ConcurrentHashMap
适用于多线程环境,尤其是写多读多的场景,它提供了更好的并发性能和线程安全保证。
-
锁机制:
ConcurrentHashMap
在JDK 1.7中使用ReentrantLock
实现分段锁,而在JDK 1.8中使用synchronized
和CAS(Compare-And-Swap)操作实现无锁或细粒度锁机制,减少了锁竞争。
-
扩容机制:
HashMap
在扩容时可能会因为并发修改导致环形链表,从而引发死循环的问题。JDK 1.8通过改变扩容机制来解决这个问题,尽管可能引起数据覆盖的问题,但不会形成死循环。ConcurrentHashMap
在扩容时保证所有线程都可以安全地访问新的或旧的桶,确保数据的一致性。
八、Hashmap 的底层结构以及它的存储流程
- 底层结构:
Java中的HashMap是一种基于哈希表的Map接口的实现。
JDK1.8 前
,底层采用数组+链表,使用的是头插法。
JDK1.8后
,底层采用数组+链表+红黑树,使用的是尾插法,避免了1.7头插法多线程操作情况下可能导致的死循环问题,当链表长度达到8时首先会判断此时数组长度是否为64,如果不为64首先会进行扩容,红黑树的话查询时比单向链表块的。 - 存储流程:
- 当向HashMap中存放键值对时,首先会通过调用键的HashCode方法,计算键的哈希值。
- 然后根据哈希值对桶的数量取余,确定键值对应该存放的桶的位置。
- 如果该位置尚无元素,则直接插入键值对。如果已有元素,则检查键是否已存在:如果键不存在冲突,则将键值对插入到链表/红黑树的末尾。
- 当链表长度超过阈值时,在JDK8及之后会将链表转换红黑树。当红黑树中的元素减少到一定数量以下,红黑树会转换为链表。(链表长度超过8就转为红黑树的设计,更多的是为了防止用户自己是实现不好的哈希算法时导致链表过长,从而导致查询效率低。)
- 当HashMap中的元素数量超过负载因子(默认为0.75倍桶的容量)时,HashMap会自动进行扩容,重新计算哈希值,然后重新分配存放位置。
九、HashMap的扩容机制
HashMap 的扩容机制是其内部操作的关键部分,它涉及到当 HashMap 中的元素数量超过一定阈值时,对内部数组(也称为桶或槽)的大小进行增加,以维持较高的搜索和插入效率。以下是 HashMap 扩容机制的详细说明:
-
负载因子(Load Factor):
HashMap 使用一个名为“负载因子”的参数来决定何时进行扩容。负载因子是一个介于 0 和 1 之间的浮点数,默认值为 0.75。扩容阈值等于当前容量与负载因子的乘积。 -
扩容阈值(Threshold):
当 HashMap 中的元素数量超过扩容阈值时,默认值为12,就会触发扩容操作。扩容阈值是当前容量与负载因子的乘积。 -
扩容操作:
扩容时,HashMap 会创建一个新的内部数组,其大小是原数组的两倍。然后,HashMap 会重新计算所有现有元素在新数组中的索引位置,并把它们迁移到新数组中。这个过程称为“重新散列”。 -
链表转换为红黑树:
在 JDK 1.8 及之后的版本中,如果一个桶(由链表表示)的长度超过一定阈值(默认为 8),那么这个链表会被转换成一个红黑树。这样可以提高搜索、插入和删除操作的效率,因为红黑树的查找时间复杂度为 O(log n),而链表的是 O(n)。 -
JDK 1.8 扩容优化:
在 JDK 1.8 中,HashMap 的扩容机制得到了优化。当进行扩容并重新分配节点时,新元素会被插入到链表的尾部。这有助于避免在 resize 过程中由于多个线程同时操作导致的链表环状问题。 -
扩容后的搜索:
在 JDK 1.8 中,即使进行了扩容,对于搜索操作,HashMap 也只需要在新数组和原数组中的一个中进行查找,这保持了搜索操作的效率。
HashMap 的扩容机制是自动的,开发者通常不需要手动触发扩容。然而,理解这一机制对于编写高效的程序和调试性能问题非常重要。
十、为啥HashMap每次扩容是扩容到原来的两倍?
HashMap每次扩容到原来的两倍,这是因为使用两倍的扩容能够更好地平衡哈希表的性能和内存消耗,并降低哈希冲突的发生。
当HashMap进行扩容时,它会重新计算元素在扩容后的新位置,然后将元素从旧位置移到新位置。在这个过程中,容量的扩大可以降低哈希冲突的概率,从而提高HashMap的性能。
比如,假设哈希表的容量是n,如果哈希表的负债因子超过了阈值(默认是0.75),HashMap就会进行扩容。而选择扩容到原来的两倍,可以保证原有的元素在新的哈希表中分布更为均匀。减少了哈希冲突的概率,提高了查询和插入的效率。
此外,扩容到原来的两倍也是为了减少扩容的频率,因为扩容是一项较为耗时的操作,减少扩容的次数可以降低在每次扩容时的开销。
综上所诉,HashMap每次扩容是扩容原来的两倍,是为了在性能和空间开销之间取得一个平衡,提高HashMapd的性能和效率。
十一、什么是泛型
Java中的泛型是从Java SE 1.5 版本引入的,它允许开发者定义参数化的类型,即在编写类、接口或方法时不指定具体的类型,而是使用类型参数来表示。这些类型参数在方法调用时被具体化,即提供了具体的类型实参。泛型消除了强制类型转换,使得代码可读性好,减少了很多出错的机会。
泛型的主要特点:
-
类型参数:在类或方法的定义中使用类型参数(尖括号< >内的名称),例如
<T>
、<E>
、<K, V>
等。 -
类型实参:在创建类实例或调用方法时,提供具体的类型作为类型参数的替代。
-
类型擦除:Java的泛型实现采用了类型擦除机制,这意味着在运行时,泛型的类型信息并不存在,因此它们可以提高性能,但同时也限制了某些运行时的操作。
-
通配符:Java泛型还支持使用问号?作为通配符,允许不确定类型的使用,但提供了一定的类型安全。
-
协变与逆变:Java泛型支持协变和逆变,这允许在子类型化的情况下更灵活地使用泛型。
-
无原始类型:Java泛型鼓励使用参数化类型,而不是原始类型(即不带任何类型参数的类或接口),以避免类型安全问题。
泛型代码示例:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.set("Hello World");
String value = stringBox.get();
}
在这个例子中,Box类是一个泛型类,它有一个类型参数T。创建Box实例时,类型参数T被具体化为String类型。
十二、ConcurrentHashMap各版本的变化
ConcurrentHashMap是Java中用于多线程并发操作的哈希表。
ConcurrentHashMap各版本变化:
- JDK1.5: 引入ConcurrentHashMap,但该版本的ConcurrentHashMap不支持扩容。在该版本的ConcurrentHashMap使用分段锁(Segment)实现,并且每个Segment都包含一个HashEntry数组和一个锁。通过将put操作分解为两步:定位Segment和在Segment上加锁再进行插入,可以实现更高的并发性能。
- JDK1.6:在此版本中,ConcurrentHashMap对扩容机制进行了优化。当有线程正在扩容时,其他线程可以继续读取原来的数据结构,从而减少了整个表被锁定的时间。此外,在JDK1.6中,ConcurrentHashMap添加了一个ConcurrentHashMap.keySet()方法,它返回一个包含map键集的ConcurrentHashMap.keySetView对象。这个KeySetView对象提供了一些额外的的功能,如removeAll()和retainAll()等。
- JDK1.7:在此版本中,ConcurrentHashMap的实现发生了一些重大改变。ConcurrentHashMap中不再使用Segment,而是采用了CAS和synchronized来保证线程安全性。同时,ConcurrentHashMap中的链表结构改为了红黑树结构,并且增加了对Map.Entry的支持。此外,JDK1.7还添加了一个ConcurrentHashMap.reduceEntries()方法,可以对哈希表中的所有键值对进行并行操作。
- JDK1.8:在此版本中,ConcurrentHashMap的实现进一步改进。在前面版本中,putVal()方法使用了多个锁来确保线程安全,这会影响并发性能。在JDK1.8中,ConcurrentHashMap使用了CAS操作来保证线程安全性,同时也提高了并发性能。此外,JDK1.8中还添加了ConcurrentHashMap.mappingCount()方法,用于返回当前映射表中的键值对数量。
ConcurrentHashMap1.8源码中,有几个特殊之处:
5. 分段锁:ConcurrentHashMap使用分段锁(Segment)来实现并发操作,每个Segment都是一个独立的哈希表,只锁定当前访问的Segment而不影响其他Segment,从而提高了并发性能。
-
CAS操作:ConcurrentHashMap使用CAS(Compare and Swap)操作来保证对同一个位置的操作的原子性。
-
安全失败机制:ConcurrentHashMap使用安全失败机制(Fail-Safe)来保证迭代器不会抛出ConcurrentModificationException异常,即当有其他线程修改了集合时,它仍然可以正常遍历,并且保证遍历结果不包含新添加的元素。
-
扩容机制:ConcurrentHashMap在扩容时,只需要对其内部的某个Segment进行扩容,而不是像HashMap那样要对整个数组进行扩容,因此扩容时的代价更小。
-
支持并发读写:ConcurrentHashMap支持多个线程同时读取和写入元素,而不需要加锁,从而充分利用了多核CPU的优势。
十三、list()和Iterate()的区别?
在编程中,List
和 iterate
通常指的是两种不同的概念,分别用于数据结构
和遍历操作
。以下是它们的主要区别:
List(列表)
-
数据结构:
List
是一种常用的数据结构,它存储了一系列的元素。在不同的编程语言中,List
可能指的是数组列表(如 Java 的ArrayList
)、链表或者其它类型的有序集合。 -
动态大小:
List
允许动态地添加和删除元素,这使得它非常适合用于存储经常变化的数据集。 -
有序:
List
中的元素是有序的,即元素的插入顺序被保留。 -
访问方式:可以通过索引来快速访问
List
中的任何元素。 -
实现:
List
接口在很多编程语言中都有实现,如 Java 的List
接口,Python 的list
类型等。
Iterate(迭代)
-
操作:
iterate
是一个动作,指的是按照一定的顺序遍历集合、列表或其他数据结构中的元素。 -
过程:迭代通常涉及获取数据结构中的每个元素,然后对每个元素执行某些操作。
-
不存储数据:迭代本身不存储数据,它是一个过程,用于访问和操作存储在其它数据结构中的数据。
-
遍历方式:迭代可以通过循环(如 for 循环、while 循环)或迭代器(如 Java 中的
Iterator
)来实现。 -
使用场景:迭代用于执行需要逐个处理数据集合中所有元素的任务。
示例区别:
-
List 示例(Java):
List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob");
-
Iterate 示例(Java):
for (String name : names) { System.out.println(name); } // 或者使用迭代器 Iterator<String> iterator = names.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); }
总结来说,List
是一种具体的数据结构,用于存储有序的元素集合,而 iterate
是一个动作,指的是遍历和处理这些元素的过程。在编程实践中,迭代通常用于访问和操作 List
或其它数据结构中的元素。
十四、请你说一下List,Set,Map三个集合的区别?
-
List
是一个有序集合,元素按照添加的顺序排列。允许存储重复的元素。通过索引访问特定位置的元素(例如get(int index)
)。常见的List
实现包括ArrayList
、LinkedList
和Vector
。 -
Set
不保证元素的顺序(不过某些实现如LinkedHashSet
和TreeSet
可以按照插入顺序或自然顺序维护元素)。不允许存储重复的元素,即每个元素都是唯一的。Set
通常通过迭代器访问。常见的Set
实现包括HashSet
、LinkedHashSet
、TreeSet
。 -
Map
存储键值对(key-value pairs),每个键映射到一个值。传统上Map
不保证元素的顺序,但LinkedHashMap
可以按照访问顺序或插入顺序维护元素。Map的每个键都是唯一的,不允许重复的键,但可以有重复的值。通过键来访问值(例如get(Object key)
)。常见的Map
实现包括HashMap
、LinkedHashMap
、TreeMap
。
需要保持元素的插入顺序,可以选择 List;需要存储唯一的元素集合,Set 是一个好选择;需要根据键来存储和检索数据,Map 是最合适的。
十五、HashSet 和 HashMap的区别
-
HashSet
是基于HashMap
实现的,确实保证了元素的无序性和不重复性。HashMap
基于数组和链表(或红黑树,当链表过长时)实现。HashMap
存储键值对,并且不允许键重复。 -
为了确保
HashSet
能够正确地检测对象是否相等,对象需要正确实现 hashCode() 和 equals() 方法。如果对象没有正确实现这些方法,那么HashSet
可能无法正常工作。HashMap
允许空键(null 作为键)和空值(null 作为值)。 -
HashSet
的检索速度并不慢,实际上,它提供了常数时间O(1)的性能(平均情况下)对于基本操作,如添加、删除和查找元素。HashMap
在大多数情况下提供常数时间O(1)的性能(平均情况下)对于基本操作,如插入和查找。 -
HashMap
不是线程安全的。如果多个线程并发访问和修改HashMap
,而没有适当的同步,可能会导致不可预知的行为。为了使HashMap
线程安全,可以使用Collections.synchronizedMap()
方法来包装它。但请注意,这种方法可能会降低性能,并且不是所有的 Map 操作方法都被同步。
十六、ArrayList是否会越界
ArrayList
在 Java 中可能会遇到 “越界”(out-of-bounds)的情况,这通常是指尝试访问一个不存在的索引位置。ArrayList
是一个基于数组实现的动态数组,它允许元素的添加、删除以及通过索引访问元素。
以下是一些可能导致 ArrayList
越界异常的情况:
-
访问不存在的索引:如果尝试访问一个超出当前列表大小的索引,将抛出
IndexOutOfBoundsException
。例如,如果列表大小为 5,尝试访问索引 5 或更高将导致异常。ArrayList<String> list = new ArrayList<>(); list.add("Element"); // 此时列表大小为 1 String element = list.get(1); // 抛出 IndexOutOfBoundsException
-
使用负索引:如果尝试使用负数作为索引访问
ArrayList
,同样会抛出IndexOutOfBoundsException
。 -
在迭代过程中修改列表:在遍历
ArrayList
的过程中,如果尝试修改列表(例如添加、删除元素),而没有采取适当的措施,可能会导致ConcurrentModificationException
。
为了避免越界异常,可以采取以下措施:
- 在访问元素之前,检查索引是否在有效范围内(即
0 <= index < list.size()
)。 - 使用
get(int index)
方法时,确保不传递超出列表大小的索引值。 - 在迭代
ArrayList
时,如果要修改列表,使用Iterator
的remove
方法或List
的removeIf
方法,或者收集要执行的操作,在迭代结束后进行。
ArrayList
的 add
方法在列表末尾添加元素时,会自动调整数组大小,所以通常不会遇到 “越界” 的问题,除非是在尝试访问不存在的索引。
Java反射(Reflection)篇
一、Java的反射机制
Java 反射是一个强大的特性,它允许程序在运行时访问、检查和操作类的对象、方法和字段。反射可以用于实现许多动态语言的功能,例如动态代理、依赖注入框架等。
反射的核心类
Class
:表示正在运行的 Java 应用程序中的类和接口。Field
:表示类中的一个字段。Method
:表示类中的一个方法。Constructor
:表示类的构造函数。
使用反射的基本步骤
-
获取
Class
对象:可以通过直接调用类型的.class
属性,或者使用Class.forName
方法来获取Class
对象。Class<?> clazz = String.class; // .class 属性 Class<?> clazz = Class.forName("java.lang.String"); // Class.forName 方法
-
访问类成员:使用
Class
对象提供的方法,可以获取类的构造函数、方法和字段。Constructor<?>[] constructors = clazz.getConstructors(); Method[] methods = clazz.getMethods(); Field[] fields = clazz.getFields();
-
创建实例:通过
Class
对象的newInstance
方法或特定的Constructor
对象的newInstance
方法来创建类的实例。Object instance = clazz.newInstance(); // 调用无参构造函数
-
调用方法和访问字段:通过
Method
或Field
对象,可以调用对象的方法或访问对象的字段。Method method = clazz.getMethod("toString"); Object result = method.invoke(instance); Field field = clazz.getField("fieldName"); Object fieldValue = field.get(instance);
反射的应用场景
- 动态代理:通过反射,可以在运行时动态地创建代理类,实现对其他对象的代理。
- 依赖注入框架:如 Spring 和 Guice,使用反射来实例化对象、管理依赖关系和配置。
- 库和框架内部:许多 Java 库和框架(如 Jackson、JPA)在内部使用反射来处理泛型、注解等。
- 测试:反射可以用来访问私有方法,进行单元测试。
注意事项
- 性能:反射操作通常比直接代码调用要慢,因为它需要动态解析和链接。
- 安全:反射可以访问私有成员,可能会破坏封装性,导致安全问题。
- 异常:反射涉及到动态类型检查,可能会在运行时抛出多种异常,如
NoSuchMethodException
、IllegalAccessException
等。
反射机制的意义
- 通过反射机制可以让程序创建和控制任何类的对象,无需提前硬编码目标类。
- 使用反射机制能够在运行时构造一个类的对象、判断一个类所具有的成员变量和方法、调用一个对象的方法。
- 反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中。
注意事项
使用反射性能较低,需要解析字节码,将内存中的对象进行解析,相对不安全,破坏了封装性。
总结
反射是一个非常强大的特性,但也需要谨慎使用,以避免不良的编程实践和潜在的性能问题。
二、什么是代理模式?
代理模式(Proxy Pattern)是一种常用的结构型软件设计模式
,其核心思想是通过一个代理对象来控制对另一个对象(即被代理对象或实际对象)的访问。代理对象在内部维护了对实际对象的引用,客户端通过代理对象间接地调用实际对象的方法。代理模式可以在不改变实际对象的情况下,为实际对象添加额外的功能,例如访问控制、延迟初始化、日志记录等。
三、代理模式
的分类
- 远程代理(Remote Proxy):为远程对象(如网络服务)提供代理,隐藏对象位于不同地址空间的事实。
- 虚拟代理(Virtual Proxy):延迟创建开销较大的实际对象,直到真正需要时才创建。
- 保护代理(Protection Proxy):用于权限检查,控制对敏感对象的访问,根据不同的访问权限提供不同的访问策略。
- 智能引用(Smart Reference):在访问对象之前执行额外的操作,如引用计数、线程安全检查等。
四、什么是动态代理
?
动态代理是指在运行时生成代理对象的技术,它可以在不知道具体被代理类的情况下创建代理对象。动态代理通常用于AOP(面向切面编程)和处理程序调用等场景。
Java 提供了动态代理机制,允许在运行时动态地创建代理类。动态代理主要通过 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口实现。
五、动态代理的实现步骤
-
定义实际对象的接口: 实际对象和代理对象都实现相同的接口。
-
创建 InvocationHandler 实现:实现 InvocationHandler 接口的 invoke 方法,在该方法中处理对实际对象的调用。
-
创建代理对象:使用 Proxy 类的 newProxyInstance 方法创建代理对象,传入实际对象的类加载器、接口数组和 InvocationHandler 实例。
-
通过代理对象调用方法:客户端通过代理对象调用方法,代理对象内部将调用转发给实际对象。
示例代码
import java.lang.reflect.*;
public class DynamicProxyDemo {
public static void main(String[] args) {
// 创建实际对象
RealSubject realSubject = new RealSubject();
// 创建 InvocationHandler 实例
InvocationHandler handler = new MyInvocationHandler(realSubject);
// 获取实际对象的类加载器
ClassLoader loader = realSubject.getClass().getClassLoader();
// 获取实际对象实现的接口
Class<?>[] interfaces = realSubject.getClass().getInterfaces();
// 创建代理对象
Subject proxyInstance = (Subject) Proxy.newProxyInstance(
loader, interfaces, handler);
// 通过代理对象调用方法
proxyInstance.request();
}
}
interface Subject {
void request();
}
class RealSubject implements Subject {
public void request() {
System.out.println("RealSubject: Handling request");
}
}
class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before: Advise before the method is called");
Object result = method.invoke(target, args);
System.out.println("After: Advise after the method has been called");
return result;
}
}
在这个示例中,MyInvocationHandler 是 InvocationHandler 的实现,它在实际对象方法调用前后添加了日志记录。客户端通过 Proxy 类创建的代理对象调用 request 方法,代理对象内部将调用转发给实际对象,并在调用前后打印日志。
六、Java动态代理实现方式
在Java中,动态代理通常有三种实现方式,分别是基于JDK的动态代理、基于CGLIB的动态代理和基于Java自带的代理类Proxy的动态代理。
JDK动态代理:运用了Java语言自带的java.lang.reflect包中的Proxy类和InvocationHandler接口,通过获取代理类的Class对象以及调用处理器来创建代理实例。
CGLIB动态代理:使用CGLIB(Code Generation Library)库,在运行时借助ASM字节码操作框架生成被代理类的子类作为代理,从而实现方法拦截和增强。
Proxy的动态代理:使用Java自带的Proxy类,结合传入的处理器对象创建代理对象,可以代理多个接口。
JDK动态代理简单用例
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 定义接口
interface Hello {
void sayHello();
}
// 实际对象
class HelloImpl implements Hello {
public void sayHello() {
System.out.println("Hello, world!");
}
}
// 实现InvocationHandler接口
class DynamicProxy implements InvocationHandler {
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method invocation");
Object result = method.invoke(target, args);
System.out.println("After method invocation");
return result;
}
}
public class Main {
public static void main(String[] args) {
Hello hello = new HelloImpl();
DynamicProxy dynamicProxy = new DynamicProxy(hello);
// 创建动态代理
Hello proxyInstance = (Hello) Proxy.newProxyInstance(
hello.getClass().getClassLoader(),
hello.getClass().getInterfaces(),
dynamicProxy);
// 调用代理对象的方法
proxyInstance.sayHello();
}
}
运行结果
在这个例子中,我们首先定义了一个Hello接口和实现了该接口的HelloImpl实际对象。然后,我们编写了一个DynamicProxy类实现了InvocationHandler接口,用于对实际对象的方法进行前置和后置处理。最后在Main类中,我们使用Proxy.newProxyInstance来创建动态代理对象,并调用代理对象的方法。
CGLIB动态代理简单示例
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
// 实际对象
class Hello {
public void sayHello() {
System.out.println("Hello, world!");
}
}
// 实现MethodInterceptor接口
class DynamicProxy implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method invocation");
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method invocation");
return result;
}
}
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Hello.class);
enhancer.setCallback(new DynamicProxy());
// 创建动态代理实例
Hello proxy = (Hello) enhancer.create();
// 调用代理对象的方法
proxy.sayHello();
}
}
在这个例子中,我们首先定义了一个Hello类作为实际对象。然后,我们编写了一个DynamicProxy类实现了CGLIB的MethodInterceptor接口,用于对实际对象的方法进行前置和后置处理。在Main类中,我们使用Enhancer类设置代理对象的超类和回调函数,从而创建动态代理对象,并调用代理对象的方法。
这个例子展示了如何借助CGLIB库来实现简单的动态代理,通过动态代理可以在不修改原有代码的情况下,实现对方法的增强和扩展。
总结
动态代理提供了一种灵活、强大的机制来控制对象的访问,可以在不修改实际对象的情况下实现多种功能扩展。
并发编程篇
一、池化技术
池化技术(Pooling)
是一种在软件架构中常用的资源管理策略,目的是减少频繁的资源分配和释放所带来的性能开销。通过重用已经创建的资源,池化技术可以提高系统的性能和响应速度。
在高并发的场景下,数据库连接数可能成为瓶颈,因为连接数是有限的。
我们的请求调用数据库时,都会先获取数据库的连接,然后依靠这个连接来查询数据,搞完收工,最后关闭连接,释放资源。如果我们不用数据库连接池的话,每次执行SQL,都要创建连接和销毁连接,这就会导致每个查询请求都变得更慢了,相应的,系统处理用户请求的能力就降低了。
因此,需要使用池化技术,即数据库连接池、HTTP 连接池、Redis 连接池等等。使用数据库连接池,可以避免每次查询都新建连接,减少不必要的资源开销,通过复用连接池,提高系统处理高并发请求的能力。
同理,我们使用线程池,也能让任务并行处理,更高效地完成任务。
二、有没有用过线程池?怎么配置的?有没有根据业务情况进行调整?
在多线程编程中,线程池是一种常用的资源管理方式,它可以有效控制资源数量,提高程序的响应速度和执行效率。
使用线程池,需要配置线程池的基本参数,可以使Java提供的类创建线程池,也可以自定义线程池参数。
-
使用Java类定义线程池:
使用ThreadPoolExecutor
或Executors
类提供的工厂方法来创建线程池。ExecutorService threadPool = Executors.newFixedThreadPool(10); // 创建一个固定大小的线程池
-
自定义线程池参数:
使用ThreadPoolExecutor
构造函数来定义线程池的具体参数。// 核心线程数 int corePoolSize = 10; // 最大线程数 int maximumPoolSize = 50; // 非核心线程空闲存活时间 long keepAliveTime = 1L; // 存活时间单位 TimeUnit unit = TimeUnit.MINUTES; // 工作队列 BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 线程工厂 ThreadFactory threadFactory = new DefaultThreadFactory(); // 饱和策略 RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler );
根据业务情况进行调整:
-
核心线程数(corePoolSize):
根据任务的类型和数量来设置。如果任务是CPU密集型的,可以设置一个相对较小的数值;如果是IO密集型的,可以设置一个相对较大的数值。 -
最大线程数(maximumPoolSize):
为了避免资源耗尽,设置一个合理的上限。 -
工作队列(workQueue):
根据任务的特性选择合适的阻塞队列,例如LinkedBlockingQueue
、ArrayBlockingQueue
或SynchronousQueue
。 -
线程存活时间(keepAliveTime)和时间单位(unit):
对于短生命周期的异步任务,可以设置较短的存活时间;对于长生命周期的后台任务,可以设置较长的存活时间。 -
线程工厂(threadFactory):
可以根据需要自定义线程名称、优先级等。 -
饱和策略(handler):
根据业务需求选择合适的饱和策略,如CallerRunsPolicy
、AbortPolicy
等。 -
监控和动态调整:
使用线程池提供的统计方法(如getActiveCount
、getCompletedTaskCount
等)来监控线程池的状态,并根据实际情况动态调整线程池参数。 -
关闭线程池:
在应用关闭时,应该调用shutdown
或shutdownNow
方法来关闭线程池,避免线程泄露。
通过合理配置线程池参数,可以最大化地利用系统资源,提高程序的并发处理能力,同时避免资源浪费和潜在的性能问题。在实际应用中,可能需要根据任务的具体特性和系统的实际表现来不断调整和优化线程池的配置。
三、线程池中,有哪些参数,分别的作用是什么
在Java中,线程池(java.util.concurrent.ThreadPoolExecutor
)是一种执行器(Executor),用于在一个后台线程中执行任务。线程池的主要目的是减少在创建和销毁线程时所产生的性能开销。线程池的核心组成部分及其作用如下:
-
核心线程数(Core Pool Size):线程池中始终保持的线程数量,即使它们处于空闲状态。
-
最大线程数(Maximum Pool Size):线程池中允许的最大线程数量。
-
工作队列(Work Queue):用于存放待执行任务的阻塞队列(BlockingQueue)。当所有核心线程都忙碌时,新提交的任务将被放入该队列中等待执行。
-
线程工厂(Thread Factory):用于创建新线程的工厂。可以自定义线程的创建过程,例如设置线程的名称或者优先级。
-
拒绝策略(Rejected Execution Handler):当任务太多,无法被线程池及时处理时,采取的策略。常见的拒绝策略包括:
AbortPolicy
:抛出RuntimeException
,阻止主线程继续执行。CallerRunsPolicy
:由调用线程(提交任务的线程)运行该任务。DiscardPolicy
:默默丢弃无法处理的任务。DiscardOldestPolicy
:丢弃工作队列中最老的任务,然后尝试再次提交被拒绝的任务。
-
保持活动时间(Keep-Alive Time):非核心线程空闲时在终止前等待新任务的最长时间。在这个时间内如果有新任务到达,线程会被重新利用。
-
时间单位(Time Unit):保持活动时间的时间单位,如秒、毫秒等。
-
线程优先级(Thread Priority):线程的优先级,通常在创建线程时设置。
四、保证多线程的线程安全有哪些方式
线程安全
,简单来说就是多线程访问同一代码,不会产生不确定的结果。
-
保证多线程安全,最简单的就是线程封闭,把对象封装到一个线程里,只有这一个线程能看到此对象,那么这个对象就算不是线程安全的,也不会产生任何线程安全问题。
-
栈封闭,简单的说就是局部变量,多个线程访问同一个方法,此方法的局部变量都会进入线程独立的局部变量中,局部变量是不会被多个线程锁共享的,也就不会出现并发问题。
-
无状态的类,即没有任何成员的类。
-
加final关键字。
-
使用Volatile,不过并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的场景。(因为写和写之间并不能保证线程安全,但是可以保证获取数据的实时性)。
-
加锁和CAS,使用synchronized关键字,使用显示锁。使用各种原子变量,修改数据使用CAS机制。
五、什么是线程死锁?怎么避免死锁,怎么解决死锁问题?
所谓死锁
,是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
死锁
产生的必要条件:
-
互斥条件,指的是资源仅有一个线程占有,若此时其他线程请求该资源,则请求线程只能等待。
-
不剥夺条件,指的是线程所获得的的资源在未使用完毕之前不能被其他资源强行剥夺,只能主动释放。
-
请求和保持条件,指的是线程已经保持了至少一个资源,但又提出了新的请求,而该资源已被其他线程占有,造成请求资源被阻塞, 但对自己获得的资源保持不放。
-
循环等待条件,指的是存在一个线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线锁所请求。
避免死锁
,一方面可以将线程按照一定的顺序加锁,另一方面可以设置加锁时限,使得线程超过一定时限就会释放找有的锁。
解决死锁
-
重新启动进行的系统,不过不建议,因为这会把参与死锁的进程以及未参与死锁的进程全部杀死。
-
撤销进程,剥夺资源。即一次性撤销参与死锁的全部进程,剥夺全部资源;或者逐步撤销参与死锁的进程,逐步收回被占有的资源,不过要按照一定的原则进行.
-
进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。
六、java线程唤醒和阻塞的五种常用方法
在Java中,线程的唤醒(唤醒等待的线程)和阻塞(使线程进入等待状态)是多线程编程中常见的操作。以下是Java中常用的五种方法来实现线程的唤醒和阻塞:
-
使用
wait()
和notify()
/notifyAll()
方法:wait()
方法用于使当前线程等待,直到另一个线程调用相同对象的notify()
(唤醒一个等待的线程)或notifyAll()
(唤醒所有等待的线程)方法。- 使用这些方法时,线程必须获取到对象的锁(通过
synchronized
关键字)。
synchronized (obj) { while (条件) { obj.wait(); } // 执行后续操作 }
-
使用
sleep()
方法:sleep()
方法让当前线程暂停执行指定的时间长度。这个方法不会释放对象的锁。
try { Thread.sleep(毫秒数); } catch (InterruptedException e) { e.printStackTrace(); }
-
使用
join()
方法:join()
方法用于等待另一个线程终止。例如,t.join()
会使得当前线程等待线程t
执行完毕。
Thread t = new Thread(() -> { // 线程t的代码 }); t.start(); t.join(); // 等待线程t结束
-
使用
Thread.sleep(long millis)
与Thread.yield()
:Thread.sleep(long millis)
让当前线程暂停指定的时间,但不释放锁。Thread.yield()
方法让当前线程放弃当前的锁,但不一定会导致其他线程立即执行。
Thread.yield(); // 建议调度器让出当前线程,但不保证
-
使用
Lock
和Condition
:java.util.concurrent.locks
包中的Lock
接口和Condition
接口提供了更灵活的线程控制。- 使用
Lock
可以代替synchronized
关键字,而Condition
允许线程等待特定的条件成立。
final Lock lock = new ReentrantLock(); final Condition condition = lock.newCondition(); lock.lock(); try { while (条件) { condition.await(); } // 执行后续操作 } finally { lock.unlock(); }
注意事项
suspend()
和resume()
这两个方法曾经是早期Java版本中Thread
类的一部分,但它们在Java 1.6之后被弃用了,因为它们存在严重的设计缺陷,可能导致死锁问题。
七、为什么suspend()
和resume()
被弃用?
-
死锁风险:如果线程在持有某个锁的状态下被挂起(
suspend()
),那么其他任何等待这个锁的线程都会永远等待下去,因为被挂起的线程不会释放它所持有的锁。 -
不可预测性:
suspend()
方法会在任何时候挂起线程,这可能导致线程处于一个不可预测的状态,比如在执行一个原子操作的中间被挂起。
由于suspend()
和resume()
的问题,Java社区推荐使用其他机制来控制线程的暂停和恢复。
在实际应用中,选择哪种方法取决于具体的使用场景和需求。例如,如果需要更细粒度的控制,可以使用Lock
和Condition
;如果只是简单的线程间通信,wait()
和notify()
可能就足够了。
八、wait()和sleep()的区别,sleep(0)可不可以有什么用
wait()
方法和sleep()
方法是用于线程控制的方法,它们有以下区别:
-
wait()是Object类的方法,用于线程间的协调和通信。sleep()是Thread类的静态方法,用于暂时让当前线程休眠一段时间。
-
调用wait()方法时,当前线程会释放对象的锁(即释放对对象的同步控制),并进入等待(WAITING)状态,直到被其他线程唤醒或等待时间到期。调用sleep()方法时,当前线程会进入阻塞(TIMED_WAITING)状态,线程不会释放任何锁。
-
在调用wait()方法前,必须先获取对象的锁(通过synchronized块或方法)。sleep()方法不需要同步块或方法的支持,可以直接在任何地方调用。
九、sleep(0)的作用
sleep(0)的确可以有一些特殊的用途,它表示当前线程暂停执行,以让其他具有相同优先级的线程有机会执行。也就是说,通过sleep(0)可以让当前线程主动让出CPU的执行时间,使得其他同优先级的线程得以执行,从而更好地进行线程调度。
需要注意的是,这并不是精确控制线程调度的方式,实际效果依赖于操作系统和具体的JVM实现。在大多数情况下,推荐使用更合适的线程调度和同步机制,而不是依赖于sleep(0)。
十、进程,线程和协程的区别
进程是计算机中运行的程序的实例
。每个进程都有自己的地址空间、内存、文件描述符、环境变量等资源。进程之间相互独立,拥有独立的内存空间,一个进程的崩溃通常不会影响其他进程。
进程
切换开销较大,需要保存和恢复大量的上下文信息。
线程是进程中的实体,用于执行程序中的代码
。一个进程可以包含多个线程,它们共享进程的资源。线程间共享进程的地址空间和其他资源,但也因此需要同步和互斥来避免资源冲突。
线程
的切换比进程的切换开销小,因为线程间共享较多资源。
协程是一种用户态线程,可以理解为轻量级线程
。它是由程序控制的,而不是由操作系统管理。协程在执行过程中可以暂停和恢复,而不是由操作系统的调度程序决定,因此可以实现用户级的并发和并行。
协程
切换开销非常小,因为它是由程序控制的,不需要保存完整的上下文。
简而言之,进程
是独立的执行实体,拥有独立的资源;线程
是进程内的执行实体,共享进程资源;协程
是一种用户态的轻量级线程,可以由程序自行控制。
十一、实现Runnable接口和Callable接口的区别?
Java中的Runnable
和Callable
接口都是用于多线程编程的接口,它们的主要区别在于返回值和抛出异常的处理方式。
-
返回值:
Runnable
接口的任务没有返回值。它只包含一个run()
方法,不接受任何参数,也不返回任何值。Callable
接口的任务可以有返回值。它包含一个call()
方法,可以返回一个Future
对象,该对象可以在未来某个时刻用来获取任务的结果。
-
异常处理:
Runnable
接口的run()
方法中的异常必须被捕获处理,因为它们不能抛出任何已检查异常。Callable
接口的call()
方法可以声明抛出已检查异常,这使得异常处理更加灵活。
-
实现方式:
- 实现
Runnable
接口的类只需要实现run()
方法。 - 实现
Callable
接口的类需要实现call()
方法,并可以返回一个Future
对象。
- 实现
-
线程执行:
- 执行
Runnable
任务时,通常使用Thread
类或者ExecutorService
的submit(Runnable)
方法,后者会返回一个Future
对象。 - 执行
Callable
任务时,使用ExecutorService
的submit(Callable)
方法,返回一个Future
对象,可以通过它获取任务的结果。
- 执行
-
用例:
- 当你只需要执行一个任务而不需要返回结果时,使用
Runnable
接口。 - 当你执行的任务需要返回结果或者需要抛出异常时,使用
Callable
接口。
- 当你只需要执行一个任务而不需要返回结果时,使用
十二、为什么我们调用start方法会调用run()方法,而不能直接调用run方法?
new一个Thread,进入新建状态,调用start方法,接着进入就绪状态,分配到时间片后就可以开始运行,start方法会执行线程的相应准备工作,自动执行run方法,完成线程工作。
如果直接执行run方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,这意味着它不会并发运行,也不会利用多线程的优势。
十三、请简要介绍一下 synchronized 关键字和 ReentrantLock 的区别和适用场景。
synchronized
关键字和ReentrantLock
都是Java中用于实现线程同步的工具,它们可以帮助解决多线程环境下的并发问题,但它们之间存在一些关键的区别:
synchronized关键字:
- 语法糖:
synchronized
是Java的关键字,可以用于修饰方法或代码块,是一种语法糖。 - 内置锁:
synchronized
是基于JVM的内置锁(Intrinsic Lock),也称为监视器锁(Monitor Lock)。 - 使用简单:使用
synchronized
很简单,只需在同步代码块或方法上加上关键字即可。 - 不可中断:使用
synchronized
时,等待锁的线程不可被中断,除非线程由于其他原因(如超时)放弃获取锁。 - 不可响应中断:线程在持有锁时,无法响应中断,可能导致死锁。
- 没有超时机制:
synchronized
没有超时机制,线程会一直等待直到获得锁。 - 实现锁:
synchronized
可以用于实现互斥锁和可重入锁。
ReentrantLock:
- 显式锁:
ReentrantLock
是java.util.concurrent.locks
包中的一个类,是一个显式锁。 - 更灵活:
ReentrantLock
提供了比synchronized
更灵活的锁定操作,例如可中断的锁获取、可超时的锁获取等。 - 响应中断:使用
ReentrantLock
时,线程在尝试获取锁的过程中可以响应中断。 - 超时机制:
ReentrantLock
提供了超时机制,即尝试获取锁时可以指定一个超时时间。 - 可实现公平锁:
ReentrantLock
可以配置为公平锁,即按照线程请求锁的顺序来获取锁,而synchronized
无法做到这一点。 - 条件变量:
ReentrantLock
提供了条件变量(Condition
)的支持,可以用于更复杂的线程间协作。 - 实现锁:
ReentrantLock
同样可以用于实现互斥锁和可重入锁。
适用场景:
synchronized
适合于简单的应用场景和对简单的同步需求,使用简单、便捷,用于保护临界资源的基本锁定和同步。
ReentrantLock
适合于对锁的灵活控制和可扩展性的应用场景,比如需要实现公平锁、可中断的锁、超时锁等功能的情况下,或者需要手动控制锁的获取和释放时。
示例:
// 使用synchronized
public class SynchronizedExample {
public synchronized void sharedMethod() {
// 线程安全的代码
}
}
// 使用ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void sharedMethod() {
lock.lock(); // 显式获取锁
try {
// 线程安全的代码
} finally {
lock.unlock(); // 显式释放锁
}
}
}
总结来说,synchronized
和ReentrantLock
各有优势和适用场景。synchronized
使用简单,适合简单的同步需求;而ReentrantLock
提供了更多的灵活性和控制能力,适合复杂的并发控制需求。
十四、Synchronized的锁升级
在Java中,Synchronized的锁升级的过程中,包括无锁状态、偏向锁状态、轻量锁状态、重量锁状态。这些步骤是为了在多线程并发情况下保证数据的安全性和一致性。
其中,锁的状态会根据线程竞争的情况逐步升级,以适应多种不同的并发场景。
无锁状态:当没有线程持有锁时,代表任何线程都可以获取到这个锁并持有它。
偏向锁状态:当只有一个线程访问同步块并获取对象的锁时,会将锁的标记记录在线程的栈帧中,并将对象头中的Thread ID设置为当前线程的ID。当这个线程再次请求相同对象的锁时,虚拟机会使用已经记录的锁标记,而不需要再次进入同步块。
轻量级锁状态:当多个线程竞争同一个锁时,偏向锁会升级为轻量级锁。轻量级锁的竞争是基于自旋CAS操作来实现的,如果自旋CAS成功,代表获取锁成功,失败则升级为重量级锁。
重量级锁状态:当自旋CAS操作一直失败时,锁会升级为重量级锁,这时会使用操作系统的互斥量来保证同步。重量级锁状态下,线程会进入阻塞状态,性能相对较低。
简单来说,从偏向锁到轻量级锁再到重量级锁,是一种逐步升级的过程,适应了不同类型的并发竞争情况。
- 当只有一个线程去争抢锁的时候,会先使用偏向锁,就是给一个标识,说明现在这个锁被线程a占有。
- 后来又来了线程b,线程c,说凭什么你占有锁,需要公平的竞争,于是将标识去掉,也就是撤销偏向锁,升级为轻量级锁,三个线程通过CAS自旋进行锁的争抢(其实这个抢锁过程还是偏向于原来的持有偏向锁的线程)。
- 现在线程a占有了锁,线程b,线程c一直在循环尝试获取锁,后来又来了十个线程,一直在自旋,那这样等着也是干耗费CPU资源,所以就将锁升级为重量级锁,向内核申请资源,直接将等待的线程进行阻塞。
十五、在并发编程中,锁的性能是一个重要的考量因素。你如何评估锁的性能?
对锁的性能进行评估时,一般可以从以下几个方面进行考量:
-
吞吐量:锁的吞吐量指的是在一定时间内能够处理的请求量,可以通过对不同锁在并发环境下的性能进行基准测试来评估。通常情况下,能够处理更多请求的锁会被认为具有更好的吞吐量。
-
延迟:锁引入的延迟是对锁性能的重要评估指标之一。通过测量加锁和释放锁所需的时间,以及在使用锁时引入的额外开销,可以评估锁对系统性能的影响。
-
公平性:对于可重入锁而言,公平性是一个重要考量因素。公平锁会按照请求锁的顺序进行处理,而非公平锁则可能会出现优先级颠倒的情况。评估锁的公平性可以从并发环境下安全性和响应时间的角度出发。
-
扩展性:锁的扩展性指的是在面对高并发或大规模需求时,锁表现出的性能表现。对于锁的扩展性可以通过对不同负载下的性能进行测试,看其在递增负载下的性能表现。
-
并发度:锁的并发度指的是锁在同一时间内能够支持的并发访问量。测试不同锁对并发度的支持情况可以评估其性能表现。
十六、CAS原理,AQS原理
CAS(Compare-And-Swap)和AQS(AbstractQueuedSynchronizer)是Java并发编程中两个重要的概念,它们被用于实现无锁编程和同步器框架。
1、CAS原理:
CAS是一种用于实现原子操作的算法,它涉及三个参数:
- 内存值V:要操作的内存中的值。
- 旧的预期值A:进行操作前,预期的值。
- 要修改的新值B:如果旧的预期值与内存值相等,则将内存值修改为这个新值。
CAS操作成功执行的条件是,当内存值V等于旧的预期值A时,将内存值更新为新值B。如果内存值在操作期间被其他线程修改了,即不等于旧的预期值A,则CAS操作失败。
优点:
- 无锁:CAS提供了一种无锁的线程安全编程方式,可以减少锁引起的开销。
- 原子性:CAS操作是原子的,即不可分割的,要么完全成功,要么完全不执行。
缺点:
- ABA问题:如果一个值原来是A,被改为B,又改回A,那么CAS检查时会发现它仍然是A,但是实际上它的值已经被改变过了。
- 性能问题:如果CAS操作失败,可能会进行多次重试,这可能导致处理器的占用率上升。
应用:Java中的java.util.concurrent.atomic
包中的原子类,如AtomicInteger
,就是基于CAS实现的。
2、AQS原理:
抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。这个类在java.util.concurrent.locks
包。用于构建锁和其他同步器的框架,它使用了一个int成员变量来表示同步状态。
核心思想:
- 状态:AQS使用一个整型变量来表示同步状态,通过该状态可以实现各种同步操作。
- FIFO队列:AQS内部使用一个双向队列(通常是一个链表)来管理那些线程的等待锁的状态。
- 模板方法:AQS定义了一系列模板方法,如
tryAcquire
、tryRelease
、tryAcquireShared
等,子类需要实现这些模板方法来定义同步器的具体行为。
优点:
- 可扩展性:AQS提供了一套完整的同步器框架,可以基于它实现各种复杂的同步组件。
- 灵活性:AQS允许开发者自定义同步器的具体行为,如可重入锁、信号量、条件变量等。
缺点:
- 复杂性:AQS的实现相对复杂,需要深入理解其内部原理才能正确使用。
应用:Java中的ReentrantLock
、Semaphore
、CountDownLatch
等同步器都是基于AQS实现的。
总结来说,CAS是一种用于实现原子操作的技术,而AQS是一个提供了一套用于构建同步器的框架。两者都是构建并发应用程序中不可或缺的工具。
十七、Java原子类
Java中的原子类是java.util.concurrent.atomic
包提供的一些用于多线程编程的类,它们支持原子操作,即不可分割的操作。这意味着这些操作在多线程环境中是安全的,不会被其他线程的操作打断。原子类主要用于实现无锁的线程安全编程,它们利用了底层硬件的原子指令,如CAS(Compare-And-Swap)。
java中的原子类大致可以分为四个类:原子更新基本类型、原子更新数组类型、原子更新引用类型;
原子更新属性类型。
类型 | 描述 |
---|---|
AtomicInteger | 基本类型原子类,提供了对int类型进行原子操作 |
AtomicLong | 基本类型原子类,提供了对long类型进行原子操作 |
AtomicBoolean | 基本类型原子类,提供了对boolean类型进行原子操作 |
AtomicIntegerArray | 原子数组类型,提供了对int数组进行原子操作 |
AtomicLongArray | 原子数组类型,提供了对long数组进行原子操作 |
AtomicReferenceArray | 原子数组类型,提供了对引用类型数组进行原子操作 |
AtomicReference | 引用类型原子类,提供了对引用类型进行原子操作 |
AtomicStampedReference | 引用类型原子类,带有版本号,可用于解决CAS操作的ABA问题 |
AtomicMarkableReference | 引用类型原子类,带有布尔标记位,用于标记节点的状态 |
十八、了解过Atomic操作类吗?底层是什么?
Java中的原子操作类位于java.util.concurrent.atomic
包中,它们是用于在多线程环境中执行原子操作的类。原子操作是指在多线程环境中,当多个线程尝试同时访问某个变量并对其进行操作时,每个操作都能在不受其他线程干扰的情况下完整地执行,即要么完全执行,要么完全不执行,不存在中间状态。
原子操作类的底层原理:
-
CAS操作:大多数原子操作类的实现依赖于
Compare-And-Swap
(CAS)指令。CAS操作涉及三个参数:内存地址(V)、预期原值(A)和新值(B)。如果内存地址处的当前值与预期原值相匹配,那么将内存地址处的值更新为新值。这个操作是原子的,由CPU指令集直接支持。 -
锁:除了CAS,某些原子操作类还可能使用锁机制来保证操作的原子性。例如,
AtomicReference
类在某些实现中可能会使用内部锁来保证引用的原子性。 -
volatile关键字:原子操作类通常与
volatile
关键字一起使用,以确保变量的可见性。volatile
变量保证了每次访问都是从主内存中读取,确保了多线程环境下变量值的一致性。 -
循环和自旋:在CAS操作失败时(即当前值不再与预期值匹配),原子操作类通常会使用循环和自旋来重复尝试执行操作,直到成功为止。自旋是一种忙等待技术,它在循环中不断检查条件直到满足。
-
无锁编程:原子操作类支持无锁编程,这意味着它们允许多个线程在没有传统锁的情况下进行协作,从而减少了线程争用导致的上下文切换和性能开销。
-
模板方法:部分原子操作类,如
AtomicStampedReference
和AtomicMarkableReference
,使用模板方法模式来实现原子操作。
示例:
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
// 使用CAS操作实现原子自增
atomicInteger.incrementAndGet();
}
}
在这个示例中,incrementAndGet()
方法内部使用CAS操作来实现原子自增,即使多个线程同时尝试执行这个操作,每个线程也能正确地对atomicInteger
的值进行递增。
总的来说,Java中的原子操作类利用了现代处理器提供的原子指令,结合volatile
关键字和循环自旋机制,提供了一种高效的方式来实现多线程中的原子操作,从而避免了使用重量级的锁机制。
十九、介绍下Volatile关键字
当一个变量被声明Volatile时,意味着这个变量可能被不同线程进行修改。这里有几个要点:
-
内存可见性:在多线程并发场景下,每个线程都有自己的工作内容,用于存储变量的副本。当一个线程修改一个变量的值时,这个修改可能保存在该线程的工作内存中而不会立即同步到主内存中。因此其他线程在读取这个变量的值时可能得到的是一个已经过时的值。而使用Volatile关键字修饰的变量,在每次修改后都会立即被写入主内存中,同时在读取时也会直接从主内存中获取最新值,这样就保证了变量的修改对于其他线程的立即可见性。
-
禁止指令重排序:在现有的处理架构中,为了提高性能往往会对指令进行重排序,这可能会影响到程序的执行顺序。而Volatile关键字也会通过内存屏障来防止编译器和处理器对指令进行重排序,从而保证了在多线程环境下变量的修改顺序对于其他线程是可见的。
-
Happens-Before 原则:Java内存模型中定义了Happens-Before原则,简而言之,如果一个写操作执行了在一个变量上,然后另一个读操作在同一个变量上被执行,那么这个写操作的执行结果将对于后续的读操作是可见的。而Volatile关键字的内存语义确保了在符合Happens-Before 原则的前提下的内存可见性。
综上所诉,Volatile关键字通过保证内存可见性和禁止指令重排序,在实际应用中,使用Volatile关键字能够提供简单的线程安全保障,特别适用于标记状态、控制并发事件以及轻量级的写入操作。然而对于复合操作,Volatile关键字的保证并不够,此时可能需要额外的加锁机制。
举个例子,假设有一个多线程环境下的计数器,我们需要对这个计数器进行自增操作,并且需要确保在一个线程对其进行增加后,其他线程可以立即看到变化。这种情况下就可以使用volatile关键字来声明计数器变量。
public class VolatileCounter {
private volatile int count;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个例子中,我们声明了一个count变量,并用volatile关键字修饰,这样可以保证在多线程环境中对count的修改操作对其他线程是可见的。可以在多个线程中同时对VolatileCounter的实例调用increment方法,从而实现线程安全的计数操作。
需要注意的是,尽管volatile可以保证可见性,但它并不保证原子性。在上述例子中,count++操作实际上是一个复合操作,包括取值、自增和’l;赋值三个步骤,这并不是原子操作。如果需要原子性的操作,可以考虑使用AtomicInteger类。
二十、介绍下公平锁和非公平锁的区别?
公平锁(Fair Lock)和非公平锁(Nonfair Lock)是两种不同的锁策略,它们在多线程同步中决定了线程获取锁的顺序。
公平锁:
-
定义:公平锁是指多个线程按照申请锁的顺序去获取锁。如果一个线程已经持有了锁,那么其他线程必须按照它们请求锁的顺序排队等待。
-
优点:
- 线程调度公平,遵循“先来先服务”的原则。
- 避免了某些线程饥饿(starvation)的情况,即长时间无法获取到锁。
-
缺点:
- 线程调度开销较大,因为需要维护一个等待队列。
- 可能会导致线程调度的效率降低,特别是在高并发情况下。
-
适用场景:当锁的持有时间较短,且线程数量较多时,使用公平锁可以减少线程饥饿现象。
非公平锁:
-
定义:非公平锁是指多个线程获取锁的顺序不按照申请锁的顺序。线程尝试获取锁时,如果锁刚好可用,它就可以立即获取锁,而不管其他线程是否已经等待了更长时间。
-
优点:
- 线程调度开销较小,因为没有维护等待队列的需要。
- 在某些情况下,可以提高吞吐量,因为线程响应更快。
-
缺点:
- 可能导致线程饥饿,特别是如果高优先级的线程频繁地请求锁,低优先级的线程可能长时间无法获取锁。
- 可能导致“线程优先级反转”的问题。
-
适用场景:当锁的持有时间非常短,且对实时性要求较高时,使用非公平锁可以减少线程调度的开销,提高性能。
Java中的实现:
在Java中,ReentrantLock
类提供了公平锁和非公平锁的选项。通过构造函数参数可以指定创建的锁是公平的还是非公平的:
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);
// 创建非公平锁
ReentrantLock unfairLock = new ReentrantLock(false);
默认情况下,ReentrantLock
是创建非公平锁。
总的来说,公平锁和非公平锁的选择取决于具体的应用场景和对线程调度公平性的需求。如果需要避免线程饥饿,可以选择公平锁;如果追求更高的吞吐量和响应速度,可以选择非公平锁。在实际应用中,需要根据具体需求和性能测试来做出选择。
二一、悲观锁和乐观锁
悲观锁(Pessimistic Locking)和乐观锁(Optimistic Locking)是两种不同的并发控制策略,用于处理多线程或多用户环境中的数据一致性问题。
悲观锁:总是假设最坏的情况,每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
-
性能:悲观锁可能会导致性能问题,因为锁的存在限制了并发性,增加了线程的等待时间。
-
优点:悲观锁可以防止数据不一致,适用于高冲突环境,即多个线程频繁修改同一数据。
-
缺点:在低冲突环境中,悲观锁可能会导致不必要的性能开销。
-
实现方式:悲观锁通常通过数据库锁机制或编程语言中的同步机制(如Java中的
synchronized
关键字和ReentrantLock
等独占锁)实现。AQS的原理也是悲观锁。
乐观锁:认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果没有被更新过,则将自己的数据写入,否则不写入。
-
性能:乐观锁通常可以提供更好的并发性能,因为它允许多个线程并发访问数据。
-
优点:乐观锁适用于低冲突环境,可以提高系统的吞吐量和响应性。
-
缺点:在高冲突环境中,乐观锁可能会导致多个线程不断重试,从而影响性能。
-
实现方式:乐观锁通常通过数据版本控制(如在数据库表中使用版本号字段)或CAS(Compare-And-Swap)操作实现。
-
悲观锁示例(数据库层面):
BEGIN TRANSACTION; SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 执行更新操作 UPDATE account SET balance = balance - 100 WHERE id = 1; COMMIT TRANSACTION;
在这个示例中,
FOR UPDATE
语句会在读取账户信息时加锁,防止其他事务修改它。 -
乐观锁示例(数据库层面):
UPDATE account SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = expected_version;
在这个示例中,
version
字段用于检测在读取数据后是否有其他事务修改了数据。如果version
不匹配,则表示数据已被其他事务修改。
总而言之,悲观锁和乐观锁的选择取决于应用场景和数据冲突的频率。悲观锁适用于写入操作多、冲突多的场景,而乐观锁适用于读多写少、冲突少的场景。在设计并发控制策略时,需要根据实际需求和性能考虑来选择最合适的锁机制。
二二、Synchronized和volatile的区别是什么?
synchronized:
- 互斥锁:
synchronized
关键字可以用于方法或代码块,确保同一时刻只有一个线程可以执行特定代码段。 - 可见性:
synchronized
确保一个线程对共享变量的修改对其他线程可见。 - 原子性:通过互斥锁,
synchronized
也确保了复合操作的原子性。 - 实现:
synchronized
可以通过修饰方法或使用 synchronized 语句块 实现。 - 性能:由于涉及到线程的阻塞和唤醒操作,
synchronized
可能会带来性能开销。 - 用途:适用于需要保证复合操作原子性的场景,如转账操作。
volatile:
- 可见性:
volatile
关键字确保变量的修改对所有线程立即可见。 - 禁止指令重排:
volatile
变量的读写操作不会被编译器重排序。 - 原子性:
volatile
只保证了对单个volatile
变量的读写操作的原子性,对于复合操作(如递增操作)则不保证原子性。 - 实现:
volatile
通过内存屏障和CPU缓存一致性协议实现。 - 性能:
volatile
相比synchronized
有更少的性能开销,因为它不会引起线程阻塞。 - 用途:适用于状态标记或旗帜(flag)等需要快速响应的场景。
简单来说, synchronized
提供了互斥锁,确保了复合操作的原子性,并且保证可见性,但可能会引起线程阻塞,适用于需要原子性的场景。 volatile
主要用于保证变量的可见性,并且禁止指令重排,但它不保证复合操作的原子性,适用于状态标记等场景。
设计模式篇
一、Java设计模式分为几大类?
设计模式是软件工程中常用的解决特定问题的模板。它们不是代码,也不是可以脱离上下文存在的设计,而是一套被实践证明了的、在特定场景下可复用的设计方法。设计模式通常分为三大类:创建型(Creational)、结构型(Structural)和行为型(Behavioral)。
1、创建型(Creational)设计模式
创建型设计模式提供了创建对象的机制,能够提升已有代码的灵活性和可重用性。常见的创建型设计模式有:
- 单例模式(Singleton):确保一个类只有一个实例,并提供一个全局访问点。
- 工厂方法模式(Factory Method):定义一个接口用于创建对象,但让子类决定要实例化的类是哪一个。
- 抽象工厂模式(Abstract Factory):提供一个接口,用于创建一系列相关或依赖对象,而不需要明确指定它们的类。
- 建造者模式(Builder):将复杂对象的构建与其表示分离,允许通过精确指定构建和表示步骤来构造复杂对象。
- 原型模式(Prototype):通过复制现有的实例来创建新的实例。
2、结构型(Structural)设计模式
结构型设计模式主要关注对象的组合和布局,以及如何将它们组合成更大的结构。常见的结构型设计模式有:
- 适配器模式(Adapter):允许将不兼容的接口转换为一个可以使用的兼容接口。
- 桥接模式(Bridge):分离一个类的行为,使它从它的形式中分离出来,使它们可以独立地变化。
- 组合模式(Composite):允许你将对象组合成树形结构以表示“部分-整体”的层次结构。
- 装饰器模式(Decorator):动态地添加另一个对象的功能,而不是创建它们子类的新的类。
- 外观模式(Facade):为子系统中的一组接口提供一个一致的界面。
- 享元模式(Flyweight):以共享的方式高效地支持大量细粒度的对象。
- 代理模式(Proxy):为其他对象提供一个代理或占位符,以控制对这个对象的访问。
3、行为型(Behavioral)设计模式
行为型设计模式主要关注对象之间的相互作用以及它们怎样相互通信,以及怎样分配职责。常见的行为型设计模式有:
- 责任链模式(Chain of Responsibility):使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合。
- 命令模式(Command):将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化。
- 解释器模式(Interpreter):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子。
- 迭代器模式(Iterator):提供一种顺序访问聚合对象元素的方法,而不暴露聚合对象的内部表示。
- 中介者模式(Mediator):定义一个中介对象来简化其他对象(同事类)之间的交互。
- 备忘录模式(Memento):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
- 观察者模式(Observer):当对象间存在一对多关系时,则使用观察者模式。一个被观察的对象变化时,所有依赖它的对象都会得到通知并自动更新。
- 状态模式(State):允许对象在其内部状态改变时改变它的行为。
- 策略模式(Strategy):定义一系列算法,把它们一个个封装起来,并使它们可以互换。
- 模板方法模式(Template Method):定义一个操作中算法的骨架,而将一些步骤延迟到子类中。
- 访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
通常认为有23种经典的设计模式,它们被收录在《设计模式:可复用面向对象软件的基础》一书中,由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides共同撰写,这四人也被称为Gang of Four(GoF)。这些设计模式是面向对象设计中常用的解决方案,但实际使用时需要根据具体情况灵活应用。
二、单例模式的实现
单例模式(Singleton Pattern)是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实例存在,对单例类的所有实例化得到的都是相同的一个实例,节约了系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
单例模式要素是私有构造方法、私有静态引用指向自己实例、以自己实例为返回值的公有静态方法。在Java中实现单例模式有几种常见的方法:
1、懒汉式(线程不安全)
- 对于懒汉式单例模式,单例实例在第一次被使用时构建,延迟初始化,相对资源利用率高。
- 缺点是当多个线程同时访问就可能同时创建多个实例,而这多个实例不是同一个对象,虽然后面创建的实例会覆盖先创建的实例,但还是会存在拿到不同对象的情况。
public class Singleton {
// 懒汉单例模式:没有用就不初始化,要用时,才初始化
private static Singleton instance;
// 私有的构造器,限制外部不能访问
private Singleton() {}
// 静态方法,获取单例
public static Singleton getInstance() {
if (instance == null) {
// 初始化
instance = new Singleton();
}
// 返回单例名
return instance;
}
}
2、懒汉式(线程安全)
为了解决线程安全问题,可以在getInstance()
方法上加锁,确保同一时刻只有一个线程可以创建实例。
public class Singleton {
// 懒汉单例模式:没有用就不初始化,要用时,才初始化
private static Singleton instance;
// 私有的构造器,限制外部不能访问
private Singleton() {}
// 静态方法,加锁,获取单例
public static synchronized Singleton getInstance() {
if (instance == null) {
// 初始化,确保同一时刻只有一个线程可以创建实例
instance = new Singleton();
}
return instance;
}
}
3、饿汉式
- 对于饿汉式单例模式,单例实例在类装载时就构建,线程安全,因为在类加载的同时已经创建好一个静态对象,调用时反应速度快。
- 缺点也很明显,资源效率不高,只要执行该类的其他静态方法或者加载了该类,这个实例仍然会初始化。
public class Singleton {
// 饿汉单例模式:在还没有实例化的时候就初始化
private static Singleton instance = new Singleton();
// 私有化的构造方法
private Singleton() {}
public static Singleton getInstance() {
// 返回单例名
return instance;
}
}
4、双重检查锁定(Double-Checked Locking,DCL)
- 双重检查锁定是一种改进的懒汉式单例模式,就是为了解决懒汉式单例模式的缺点的。使用了synchronized关键字对实例初始化前后进行加锁。同时通过双重检查确保实例只被创建一次,避免了每次调用
getInstance()
时的同步开销。 - 缺点是第一次加载时反应不快,多线程使用不必要的同步开销大。
public class Singleton {
// 设置可见变量
private static volatile Singleton instance;
// 私有化的构造方法
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查
if (instance == null) {
// // 加锁保证一次运行一个
synchronized (Singleton.class) {
// 第二次检查
if (instance == null) {
// 加锁保证instance为空时,创建一个实例
instance = new Singleton();
}
}
}
return instance;
}
}
5、静态内部类
- 静态内部类单例模式利用了Java的类加载机制来实现单例模式,资源利用率高,单例实例在第一次使用时构建,延迟初始化。
- 缺点是第一次加载时反应不够快。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
6、枚举
使用枚举(enum)来实现单例模式是一种简洁且线程安全的方式,Java语言规范保证每个枚举常量只会被实例化一次。
public enum Singleton {
INSTANCE;
public void someMethod() {
// 一些方法
}
}
对于单例模式,一般采用饿汉式,若对资源十分在意可以采用静态内部类,不建议采用懒汉式及双重检测。不过选择哪种单例模式实现方式取决于具体需求,例如是否需要延迟加载、是否面临多线程环境等。在单例模式的实现中,考虑线程安全和性能是非常重要的。
三、单例模式的使用场景?
根据单例模式的特点,它的使用场景可以分为如下几个:
-
配置管理器:当一个应用程序需要一个全局配置信息时,例如数据库连接信息、文件系统配置等,单例模式可以确保配置信息全局只有一个实例,并且所有线程都能访问到这个实例。
-
线程池:线程池需要限制创建的线程数量,单例模式可以确保整个应用程序中只存在一个线程池实例。
-
缓存:在需要缓存数据以避免重复计算或数据库查询的场景中,单例模式可以确保缓存全局只有一个实例,所有请求都使用同一个缓存。
-
注册表:在需要跟踪或注册应用程序中的单个组件或服务时,例如跟踪所有打开的文件或网络连接,单例模式可以确保注册表全局只有一个实例。
-
设备管理器:当应用程序需要访问特定的硬件设备,如打印机、扫描仪等,单例模式可以确保设备管理器全局只有一个实例,以便于管理和访问控制。
-
日志记录器:日志记录器通常需要全局访问,单例模式可以确保应用程序中只存在一个日志记录器实例,所有日志信息都通过这个实例进行记录。
-
对话框:在GUI应用程序中,对话框如“关于”或“设置”对话框通常只需要一个实例,单例模式可以确保无论何时调用,都只打开一个对话框。
-
应用状态:应用程序的状态管理,如用户登录状态、全局进度状态等,可以使用单例模式来确保状态全局一致性。
-
资源管理器:在需要管理共享资源,如内存、显存或其他关键资源时,单例模式可以确保资源管理器全局只有一个实例,以避免资源冲突。
-
服务定位器:服务定位器模式(Service Locator pattern)中,可以使用单例模式来提供一个全局访问点,用于查找和访问应用程序中的服务。
四、简单介绍下工厂模式
工厂模式(Factory Pattern)是一种常用的软件设计模式。该模式用来封装和管理类的创建,目的是为了解耦,实现创建者和调用者的分离。工厂模式的本质就是对获取对象过程的抽象。工厂模式可以分为以下几种主要类型:
1、简单工厂模式(Simple Factory Pattern)
简单工厂模式是将对象交由一个新的类来创建,这种类叫工厂类。这个工厂类会根据传入的参数决定实例化哪个具体类。调用者只需要调用这个类的函数来创建对象就行了,无需自己书写创建对象的函数。
优点:
- 封装性:客户端不需要知道具体的类是如何实现的,只需要知道工厂类和参数即可。
- 易于扩展:新增类不需要修改已有的工厂类和其他客户端代码。
缺点:
- OCP违背开闭原则:新增产品需要修改工厂类的决策逻辑,这违背了对扩展开放,对修改封闭的原则。
2、工厂方法模式(Factory Method Pattern)
工厂方法模式是简单工厂模式的改进,它定义了一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法让类的实例化推迟到子类中进行。
优点:
- 遵循开闭原则:新增产品不需要修改已有代码,只需要增加相应的具体类和工厂子类。
- 更好的封装性:工厂方法模式隐藏了具体的类名。
缺点:
- 每增加一个产品类别,就需要增加一个具体类和产品类对应的工厂类,这可能会导致类数量急剧增加。
3、抽象工厂模式(Abstract Factory Pattern)
抽象工厂模式提供一个接口,用于创建一系列相关或相互依赖的对象,而不需要指定它们具体的类。它主要用于当一个系统不应当依赖于产品的具体实现,而仅依赖于它们之间的接口时。
优点:
- 更好的封装性:抽象工厂模式封装了产品系列的创建逻辑。
- 更容易扩展:增加一个新的产品系列,只需要增加一个具体的工厂类。
缺点:
- 系统的复杂性:每增加一个产品系列,都需要增加一个抽象工厂和多个具体工厂类。
4、原型模式(Prototype Pattern)
原型模式允许通过拷贝现有的实例(原型)来创建新的实例,而不是通过新建实例来实现。
优点:
- 创建新对象的成本较高时,可以通过复制现有对象来减少开销。
- 可以快速地创建复杂的对象。
缺点:
- 如果对象包含不可变或者不稳定的资源,复制可能会导致问题。
示例:
// 简单工厂模式示例
public class SimpleFactory {
public Product createProduct(String type) {
if (type == "A") {
return new ConcreteProductA();
} else if (type == "B") {
return new ConcreteProductB();
}
return null;
}
}
// 工厂方法模式示例
public abstract class Creator {
public abstract Product factoryMethod();
}
public class ConcreteCreatorA extends Creator {
@Override
public Product factoryMethod() {
return new ConcreteProductA();
}
}
// 抽象工厂模式示例
public interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}
public class ConcreteFactory implements AbstractFactory {
public ProductA createProductA() {
return new ConcreteProductA();
}
public ProductB createProductB() {
return new ConcreteProductB();
}
}
工厂模式主要用于解耦对象的创建和使用,使得扩展和修改更加灵活。在实际应用中,需要根据具体的需求和场景选择合适的工厂模式。
扩展
OCP(Open-Closed Principle),它的核心含意是:一个好的设计应该能够容纳新的功能需求的增加,但是增加的方式不是通过修改原有的模块(类),而是通过增加新的模块来完成的,也就是在设计的时候,所有软件组成实体包括接口,函数等必须是可扩展但不可修改的。
JAVA新特性
一、JDK6、7、8分别提供了哪些新特性
JDK 6 新特性
JDK 7的新特性
JDK8 的新特性
二、jdk1.8有哪些新特性?
-
Lambda表达式:允许把函数作为一个方法的参数
-
函数式接口:内部只有一个函数,提供了@FunctionalInterface注解;jdk默认提供了一些函数接口供我们使用:
- Function类型接口,代表的是有参数,有返回值的函数。
- Consumer类型接口,不返回任何结果。
- Predicate系列接口,接收T类型,返回boolean类型结果。
- Supplier系列(供应者)不接受任何参数,返回T类型结果。
-
方法引用:可以将存在的方法作为变量来传递使用。例如:类名::静态方法名、类型::非静态方法名、实例对象::非静态方法名、类名::new。
接口的默认方法和静态方法- 默认方法使得开发者可以在不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。
- 默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写。由于JVM上的默认方法的实现在字节码层面提供了支持,因此效率非常高。
- 默认方法允许在不打破现有继承体系的基础上改进接口。
- Java 8带来的另一个有趣的特性是在接口中可以定义静态方法,我们可以直接用接口调用这些静态方法。
-
Optional容器,可以存放T类型的值或者null。
- 如果 Optional 实例持有一个非空值,则 isPresent() 方法返回 true ,否则返回 false 。
- 如果 Optional 实例持有null ,orElseGet() 方法可以接受一个lambda表达式生成的默认值。
- map() 方法可以将现有的 Optional 实例的值转换成新的值。
- orElse() 方法与 orElseGet() 方法类似,但是在持有null的时候返回传入的默认值,而不是通过Lambda来生成。
-
Streams:新增的Stream API(java.util.stream)将生成环境的函数式编程引入了Java库中。这是目前为止最大的一次对Java库的完善,以便开发者能够写出更加有效、简洁和紧凑的代码。
-
并行数组:Java8版本新增了很多新的方法,用于支持并行数组处理。最重要的方法是 parallelSort() ,可以显著加快多核机器上的数组排序。
数据结构篇
一、深拷贝,浅拷贝
深拷贝和浅拷贝是在计算机编程中常用的两种数据复制技术。
浅拷贝(shallow copy)
指复制一个对象,创建一个新的对象,但并不会复制该对象内部包含的子对象。简单来说,浅拷贝只是复制了对象本身,而没有复制对象内部的引用类型数据,因此原对象和新对象之间共享同一份引用类型数据。如果其中一个对象修改了这个引用类型数据,另一个对象也将受到影响。
深拷贝(deep copy)
是完全复制一个对象,包括其内部所有的子对象,每个对象都有自己的一份独立的数据,修改其中一个对象的数据不会影响其他对象。深拷贝会递归地复制整个对象树,因此比浅拷贝要耗费更多的时间和空间。
总结
在实际应用中,我们需要根据具体情况选择使用深拷贝或浅拷贝。通常来说,如果数据结构较为简单且不包含引用类型数据,我们可以使用浅拷贝;如果数据结构较为复杂且包含引用类型数据,我们则需要使用深拷贝来避免数据共享带来的问题。
二、BeanUtils.copyproperties 底层原理
BeanUtils.copyProperties
方法是 Apache Commons BeanUtils
库中提供的一个工具方法,用于将一个 Java 对象的属性值复制到另一个 Java 对象中。
底层原理
在 BeanUtils.copyProperties
方法内部,首先获取源对象和目标对象的属性描述符(PropertyDescriptor),然后遍历源对象的属性描述符,对于每个属性,BeanUtils.copyProperties
方法会调用源对象的 getter 方法获取属性值,然后调用目标对象的 setter 方法将该属性值设置到目标对象中,从而实现属性值的复制。
需要注意的是, BeanUtils.copyProperties
方法只能复制两个对象中存在相同属性名的属性值。如果存在属性名不同但属性类型相同的属性,可以使用 BeanUtils.copyProperties
方法进行单个属性的复制;如果存在属性名不同且属性类型不同的属性,就需要手动编写转换方法来完成属性的复制。
三、BeanUtils.copyProperties
方法 属于浅拷贝
在 BeanUtils.copyProperties
方法中,如果源对象和目标对象的属性类型相同,则直接将源对象的属性值赋给目标对象。这意味着,如果属性值是一个对象引用,那么目标对象和源对象将共享同一个对象实例,即它们指向同一块内存地址。因此,当其中一个对象修改该属性值时,另一个对象的属性值也会随之改变,这就是浅拷贝的特点。
需要注意的是,如果源对象和目标对象的属性类型不同,BeanUtils.copyProperties
方法会尝试进行类型转换(Type Conversion)来完成属性值的复制。但是,如果无法进行类型转换,这个过程可能会抛出异常。
如果需要进行深拷贝,即复制对象及其所有引用的子对象,可以使用其他工具类或手动实现递归拷贝。
四、为什么不推荐使用BeanUtils.copyProperties?
BeanUtils.copyProperties
是 Spring Framework 提供的一个实用工具方法,用于将一个 Java Bean 的属性复制到另一个 Java Bean。尽管这个方法在某些情况下很有用,但在使用时也有一些潜在的问题,这就是为什么不推荐在所有情况下都使用它的原因:
-
性能问题:
BeanUtils.copyProperties
在内部使用反射进行属性复制,这可能会导致性能开销,特别是当复制大量对象或在性能敏感的应用中时。 -
类型安全:如果源对象和目标对象的属性类型不完全匹配,
BeanUtils.copyProperties
仍然会尝试进行自动类型转换,这可能会导致意外的结果或运行时错误。 -
忽略注解:使用
BeanUtils.copyProperties
时,它不会考虑目标对象上的任何特定注解,如@JsonIgnore
或@Transient
,这可能导致不需要的属性被复制。 -
自定义逻辑:如果需要在复制过程中执行自定义逻辑(例如,验证、转换或忽略某些属性),
BeanUtils.copyProperties
可能不够灵活。 -
缺少 null 检查:如果源对象为
null
,BeanUtils.copyProperties
会抛出IllegalArgumentException
,这可能需要额外的错误处理。 -
错误消息:当属性复制失败时,
BeanUtils.copyProperties
提供的错误消息可能不够详细,难以调试。 -
单例问题:如果不小心,可能会将单例对象的引用复制到多处,导致不一致的状态。
-
版本兼容性:依赖于第三方库(如 Spring)可能会导致版本兼容性问题,特别是在升级库版本时。
-
过度依赖框架:过度依赖 Spring 或其他框架的实用工具方法可能会使代码过于依赖于特定的框架,降低其可移植性。
五、BeanUtils.copyProperties的替代方案
- 使用其他库:如 Apache Commons BeanUtils、MapStruct 或 ModelMapper,这些库可能提供了更高性能或更多功能的替代方法。
- 手动复制:在对象属性较少且关系简单的情况下,手动编写属性复制代码可能是一个更可控和性能更好的选择。
- 使用构建器模式:构建器模式可以提供一个更清晰和类型安全的方式来创建和复制 Java Bean。
在使用 BeanUtils.copyProperties
或任何自动 Bean 复制工具时,重要的是要了解它们的限制,并在适当的时候选择最适合项目需求的方法。
六、零拷贝是什么
零拷贝(Zero Copy)
是一种技术,可用于优化数据传输过程中的性能和效率。它通常用于减少系统中不必要的复制和上下文切换操作,以提高数据传输速度和减少CPU负载。
在传统的数据传输方式中,当一个进程想要将数据从一处传输到另一处时,它需要将数据从应用程序的内存复制到内核缓冲区中,然后再从内核缓冲区复制到网络适配器的缓冲区中。这种复制会增加CPU负载和内存带宽的使用,并且会导致额外的上下文切换操作。
零拷贝技术
通过直接访问应用程序内存来避免这些复制操作。应用程序可以将数据指针直接传递给内核或网络适配器,从而避免了不必要的复制和上下文切换操作。这可以提高数据传输的效率,减轻CPU负载,同时还可以降低内存带宽的使用。
零拷贝技术
常见的应用场景包括网络数据传输、磁盘I/O、共享内存等场景。
Java Web篇
一、Servlet是什么?
Servlet 是 Java EE(Java Enterprise Edition)规范的一部分,它是一种运行在服务器端的 Java 程序,用于处理客户端请求(通常是 HTTP 请求)并生成响应。Servlet 提供了一种方式来创建 Web 应用程序,它在 Web 服务器或应用服务器上运行,扩展了服务器的功能。
二、Servlet
的主要特点
-
遵循规范:Servlet 必须遵循 Java EE Servlet 规范,这确保了它在不同的服务器上具有很好的移植性。
-
生命周期:Servlet 有一个明确的生命周期,包括加载、初始化、请求处理、服务和销毁。
-
多线程:Servlet 可以处理多个并发请求,因为服务器可以为每个请求创建多个线程。
-
使用 HTTP 协议:Servlet 主要用于处理 HTTP 请求,但也可以处理 HTTPS 请求。
-
使用请求(Request)和响应(Response)对象:Servlet 通过请求对象获取客户端发送的信息,通过响应对象发送数据回客户端。
三、Servlet
的生命周期
-
加载:Servlet 容器(如 Tomcat)根据 Web 应用的配置加载 Servlet 类。
-
初始化:通过调用 Servlet 的
init
方法初始化 Servlet。可以传递ServletConfig
对象作为参数,它包含了 Servlet 的初始化参数。 -
请求处理:Servlet 容器为每个请求创建一个
request
和response
对象,然后调用 Servlet 的service
方法来处理请求。 -
销毁:当 Servlet 的生命周期结束时,容器调用
destroy
方法。 -
卸载:Servlet 从内存中卸载。
四、创建 Servlet
的步骤
- 实现 Servlet 接口:编写一个类实现
javax.servlet.Servlet
接口,并实现service
、init
和destroy
方法。 - 配置 Servlet:在 Web 应用的
web.xml
配置文件中声明 Servlet,并映射一个或多个 URL 模式。 - 部署 Servlet:将 Servlet 打包为 WAR 文件,并部署到 Servlet 容器。
示例代码
import javax.servlet.*;
import java.io.*;
public class MyServlet implements Servlet {
public void init(ServletConfig config) throws ServletException {
// 初始化代码
}
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
// 处理请求和响应的代码
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("Hello, World!");
}
public void destroy() {
// 清理代码
}
public ServletConfig getServletConfig() {
// 返回 ServletConfig 对象
return null;
}
public String getServletInfo() {
// 返回 Servlet 的信息
return "This is MyServlet";
}
}
配置 Servlet
<web-app ...>
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>com.example.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/my</url-pattern>
</servlet-mapping>
</web-app>
在这个例子中,当客户端发送一个以 /my
开头的请求时,Servlet 容器将请求映射到 MyServlet
。
注意事项
-
线程安全:Servlet 是多线程的,因此需要确保 Servlet 的代码是线程安全的。
-
异常处理:Servlet 需要妥善处理所有可能的异常,否则可能会导致 Servlet 容器无法正常响应请求。
-
安全性:Servlet 需要考虑到安全性,如验证用户输入,防止跨站脚本攻击(XSS)和 SQL 注入等。
五、Servlet
安全性问题
因为servlet是单列模式创建的
,只实例化一次,同一个servlet可以处理多个用户请求,当同时有两个用户访问时,则会启动两个负责处理请求的servlet线程,所以会出现线程安全问题。
** 解决方案:**
- 在servlet中定义变量时,尽量都定义局部变量。在servlet中负责保存上下文ServletContext和负责处理session对象的HttpSession是线程不安全的,而负责处理请求的servletRequest是线程安全的
- 加锁:用synchronized进行保护,但是要尽量的缩小保护范围
总结
Servlet
为构建动态和交互式的 Web 应用程序提供了强大的基础,并且是 Java Web 技术的核心部分。随着 Java EE 的发展,Servlet 规范也在不断更新,以支持新的功能和改进。
Java虚拟机篇
一、JVM的底层原理
JVM在整个jdk(Java运行环境)中处于最底层,负责与操作系统的交互,用来屏蔽操作系统环境,提供了一个完整的Java运行环境。说到JVM底层,最主要的就是内存模型和垃圾回收机制。
内存模型
JVM内存模型
主要分为堆Heap
、方法区
、虚拟机栈
、本地方法栈
、程序计数器
。jdk1.8去掉了方法区,直接在内存中写入原空间。
方法区
被称为永久代,用户存储虚拟机加载的类信息、常量、静态变量,是各个线程的共享的内存区域。Java堆
,也叫GC堆,是JVM中所管理的内存中最大的一块内存区域,是线程共享的,在JVM启动时创建,存放了对象的实力及数组。虚拟机栈
,描述的是Java方法执行的内存模型,每个方法被执行的时候,都会创建一个“栈帧”,用于存储局部变量、操作栈、方法出口等信息。每个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,生命周期与线程相同,是线程私有的。本地方法栈
,与虚拟机栈基本类似,为本地方法服务。程序计数器
,是最小的一块内存,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、异常处理、线程恢复等基础功能都需要依赖计算器完成。
对于各内存区域回收,方法区低于5%,Java堆会回收70%~95%,Java栈是100%回收,程序计数器不回收,本地方法栈100%回收。
垃圾回收机制
垃圾回收机制
,主要包括垃圾回收算法
和垃圾收集器
。
主要算法有:标记-清除算法(Mark-Sweep)
、复制算法(Copying)
、标记-整理算法(Mark-Compact)
、分代收集算法(Generational Collection)
。
其中分代收集算法是目前大部分JVM垃圾收集器采用的算法,核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域)。
垃圾算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。主要的垃圾收集器有:
Parallel Scavenge
,是一个新生代的多线程收集器,在回收期间不需要暂停其他用户线程,采用的是Copying算法
。Parallel Old
,是Parallel Scavenge收集器的老年代版本,使用多线程和Mark-Compact算法
。CMS收集器
,是一种以获取最短回收停顿时间为目标的收集器,是一种并发收集器,采用的是Mark-Sweep算法
。G1
,是当今收集器技术发展最前沿的成果,是一款面向服务端应用的收集器,能充分利用多CPU、多核环境。能建立可预测的停顿时间模型。
对象内存的分配与回收
除此之外,JVM底层还有对象内存的分配与回收
。
- 对于分配,大部分对象在分配时都是在
Eden
中,较大的对象直接分配到Old Generation
中。 - 对于回收,
新生代GC(Minor GC)
,发生在新生代的垃圾回收动作,因为大多数对象都是朝生暮死的,所以Minor GC
非常频繁,回收速度也比较快。老年代GC(Major GC/Full GC)
发生在老年代的GC,发生Full GC
时,一般会伴随着一次Minor GC
,Full GC
的速度比较一般,会比Minor GC
慢10倍以上。
对象的访问和对象引用强度
对象访问
涉及到Java栈、Java堆、方法区三个内存区域。对象访问
有两种方式,句柄访问方式
和指针访问方式
。
-
句柄访问方式
中存储的就是对象的句柄,句柄中包含了对象的实例数据和类型数据各自的具体地址信息。 -
指针访问方式
变量中直接存储的就是对象的地址,Java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据的地址。 -
使用
句柄访问地址
最大好处就是reference中存储的是稳定的句柄地址,在对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。使用指针访问方式
最大好处就是速度快,它节省了一次指针定位的时间开销,就虚拟机而言,它使用的是第二种方式。
对象的引用程度
对象的引用程度分为了强引用
,软引用
,弱引用
和虚引用
。
强引用
类似C语言中的指针,可以直接访问目标对象,是平常中使用最多的引用,内存不足也不会被回收。软引用
,是除了强引用外,最强的引用类型,可以通过Java.lang.ref.SoftReference类来实现。当GC认为扫描的SoftReference不经常使用时,会进行回收。适合于创建缓存的时候,创建的对象放进缓存中,内存不足时GC就回收掉。弱引用
,是一种比较软引用的引用类型,只要GC工作时,无论内存是否足够,都会回收掉,通过WeakReference类实现。适用于当不需要某个对象时,可以直接用弱引用创建对象,JVM一旦发现就会自动处理掉它,不需要我做其他操作。虚引用
,是所有类型中最弱的一个,虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue 。
内存调优
过多的GC和Full GC会占用很多的系统资源,影响系统的吞吐量,为了减少Full GC次数,减少GC频率,尽量降低GC所导致的应用线程暂停时间,就需要针对内存管理方面进行调优,包括控制各个代的大小,使用GC策略。
类加载机制
一个类需要经过:加载-》验证-》准备-》解析-》初始化-》使用-》卸载这些阶段。
ClassLoader加载的原理是使用了双亲委派模型
来搜索类,每个ClassLoader实例都有一个父类加载器的引用,当父亲已经加载了该类的时候,子ClassLoader不再加载,避免了重复加载。
二、Java1.8版本以后不再使用方法区的原因
Java 1.8版本以后不再使用方法区,主要有以下几个原因:
回收效率低下
:在Java 1.8之前,方法区负责存储类信息、常量池、静态变量、即时编译的代码等,但方法区的回收效率较低,垃圾回收不能很好地处理这些数据,导致内存占用过高。与堆内存合并
:Java 1.8的元空间(Metaspace)取代了方法区,将类的元数据存放在本地内存中,这样可以更好地利用系统资源,也能更方便地与堆内存进行交互和竞争。动态扩展
:元空间可以动态地调整大小,根据实际需求进行内存分配,而方法区的大小是固定的,无法动态扩展。性能优化
:元空间的设计优化了类的加载和卸载过程,提升了类的加载和卸载性能,减少了类加载过程对系统性能的影响。
总的来说,Java 1.8版本以后不再使用方法区,而是采用元空间来存储类的元数据,这样做的主要目的是为了提高性能、优化内存管理,并提供更灵活的内存使用方式。
三、堆内存、占栈内存
在JVM中,内存被分为几个不同的区域,每个区域都有特定的用途:
堆内存(Heap Memory)
堆内存分为为年轻代(伊甸区和幸存区)
、老年代
,主要用途是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存,垃圾收集器就是收集的这些对象,然后根据GC算法回收。
-
年轻代(Young Generation):这是新对象创建的地方,分为两个主要区域:
- 伊甸区(Eden Space):大多数新对象首先在这里分配。
- 幸存区(Survivor Space):当伊甸区满时,会触发一次Minor GC,存活下来的对象会被移动到幸存区。
-
老年代(Old Generation):经过多次GC后仍然存活的对象会被提升到老年代。老年代用于存放长期存活的对象。
-
永久代(Permanent Generation,Java 8之前)/元空间(Metaspace,Java 8及之后):用于存放类的元数据,如类定义、常量池等。Java 8开始,永久代被元空间取代,元空间使用的是本地内存(Native Memory)而非堆内存。
栈内存(Stack Memory)
栈内存主要用途是存放基本类型数据,如int、long、byte、float、double、Boolean、char(没有String)和对象句柄。在栈内存的数据大小及生存周期是必须确定的。
- 局部变量表:存放基本类型的数据和对象引用(句柄)。
- 局部变量的生命周期:与方法的调用周期相同,方法调用结束,局部变量的生命周期也随之结束。
栈与堆的区别
- 存储内容:栈存放局部变量和部分执行上下文,堆存放对象实例和数组。
- 分配方式:栈内存由JVM自动管理,分配和回收速度快;堆内存需要程序员手动管理(通过new和对应的垃圾回收机制)。
- 生命周期:栈内存的生命周期与方法的调用周期相同,方法结束,栈内存自动释放;堆内存对象的生命周期不固定,由GC机制决定。
- 大小限制:栈的大小通常较小,且对每个线程都是独立的;堆的大小通常较大,是所有线程共享的。
四、为什么要进行垃圾回收
垃圾回收(Garbage Collection,GC)是自动内存管理的关键部分,特别是在使用如Java这样的高级编程语言时。进行垃圾回收的主要原因:
-
防止内存泄漏:程序在运行过程中可能会动态分配内存,如果不正确地管理这些内存,可能会导致内存泄漏,即不再使用的内存没有被释放,导致可用内存减少,最终可能使程序崩溃或系统变慢。垃圾回收可以自动检测并回收这些不再使用的对象,防止内存泄漏。
-
提高内存使用效率:垃圾回收可以释放那些不再被引用的对象所占用的内存,从而使得内存可以被重新分配给新的或现有的对象,提高内存的利用率。
-
简化编程:开发者不需要手动管理内存的分配和释放,这减少了编程的复杂性,并允许开发者更专注于业务逻辑的实现。
-
避免野指针:野指针是指向已经释放或未初始化内存的指针,它们的存在可能导致程序运行不稳定或产生不可预测的结果。垃圾回收机制通过自动回收无用对象,减少了野指针出现的可能性。
-
内存分配的动态性:在一些应用场景中,程序的内存需求可能会随着时间变化,垃圾回收允许程序根据当前的内存需求动态地分配和回收内存。
-
优化内存布局:某些垃圾回收算法(如标记-整理)在回收内存的同时,会整理内存空间,减少内存碎片,提高内存分配的效率。
-
支持多线程环境:在多线程环境下,手动管理内存可能会导致同步问题,垃圾回收机制通常设计有线程安全的特性,可以在多线程环境中有效地工作。
-
提升性能:虽然垃圾回收可能会引起程序的短暂停顿,但是现代垃圾回收器通过各种优化技术(如分代收集、并行/并发收集等)已经可以将这种影响降到最低,并且通过合理地回收内存,长期来看可以提升程序的整体性能。
-
资源的公平分配:在多用户或多任务的环境中,垃圾回收可以确保每个任务都能公平地使用内存资源,避免某些任务占用过多内存而影响其他任务。
垃圾回收是现代软件开发中的一个重要特性,它极大地提高了软件的稳定性和开发效率。然而,理解垃圾回收的行为对于编写高效的程序和调优性能也是非常重要的。
五、如何进行垃圾回收优化和调优
进行垃圾回收优化和调优可以提高程序的性能和响应速度,同时也能够避免因为内存溢出等问题导致程序异常。
- 选择合适的垃圾回收器:JVM提供了多种不同类型的垃圾回收器,如串行垃圾回收器、并行垃圾回收器、CMS垃圾回收器等。根据应用的场景和特点,选择合适的垃圾回收器可以有效地提升程序的性能和响应速度。
- 调整堆大小:堆是Java程序中最重要的内存区域,堆的大小会影响到垃圾回收的效率和性能。通常情况下,增加堆的大小可以减少垃圾回收的频率和时间,但过大的堆也可能会导致垃圾回收时间过长。
- 使用分代垃圾回收机制:把JVM的堆内存分为新生代和老年代可以提高垃圾回收的效率。新生代采用复制算法,老年代采用标记-清除或标记-整理算法。通过对新生代和老年代的不同处理,可以更好地平衡垃圾回收效率和内存占用。
- 确定垃圾回收的时间:合理地调整垃圾回收的时间可以避免在程序高峰期影响程序的性能。通常情况下,尽可能等待堆满后再进行垃圾回收。
- 确定垃圾回收的方式:不同的垃圾回收算法和方式会对程序性能产生不同的影响。应该根据应用场景和特点选择合适的垃圾回收算法和方式。
深入理解JVM的内存模型、垃圾回收算法、垃圾回收器和性能调优策略对于构建高性能的Java应用程序至关重要。通过监控工具和性能分析,可以识别性能瓶颈并采取适当的措施进行优化,以确保应用程序的稳定性和性能。
六、如何避免内存泄露
内存泄露是指在程序执行过程中,一些不再被使用的对象仍然存在于内存中,导致内存空间的浪费和程序性能下降。以下是一些避免内存泄露的常见方法:
-
及时释放资源:在使用完毕后,要及时释放对象占用的资源,如关闭文件、数据库连接等。可以使用try-with-resources语句块来自动释放资源。
-
尽量避免使用静态变量:静态变量往往会持有对象的引用,在不需要使用的时候没有及时释放,容易引起内存泄露。因此,尽量避免使用静态变量,或者在适当的时候手动清除它们的引用。
-
使用弱引用和软引用:在JVM中,可以使用弱引用和软引用来引用一些不重要的对象,在内存不足时可以被自动回收。使用弱引用和软引用可以有效地避免内存泄露问题。
-
避免循环引用:循环引用是指两个或多个对象相互引用,但没有任何一个对象被其他对象所引用,导致这些对象不能被垃圾回收器自动回收,从而造成内存泄漏。为避免循环引用,可以使用弱引用或者手动打破循环引用关系。
-
使用合适的数据结构:选择能够自动管理内存的数据结构,例如使用ArrayList代替Vector,因为ArrayList比Vector更节省内存,且不会持有对其元素的不必要引用。
-
单例模式慎用:单例持有许多全局状态信息,这可能导致内存泄露。如果使用单例,确保它不会持有长时间存活的对象引用。
-
避免不必要的缓存:缓存可以提高性能,但也可能导致内存泄露,特别是当缓存存储了大量长时间存活的对象时。
-
使用内存分析工具:使用内存分析工具(如MAT、VisualVM等)来识别内存泄露的模式和根源。
-
使用finalize()方法:虽然不推荐使用finalize()方法进行资源清理,但在某些特定情况下,它可以作为一个最后的补救措施来释放资源。
通过遵循上述策略,可以显著减少Java应用程序中的内存泄露风险。然而,完全避免内存泄露需要持续的关注和维护,以及对应用程序内存使用模式的深入了解。
网络编程篇
一、项目中流量染色是什么
流量染色(Traffic Shaping)
是一种网络管理技术,用于限制数据包在网络中的传输速率和优先级,以便控制网络拥塞并优化网络性能。
在项目中,流量染色通常用于控制不同类型的网络流量,例如视频、音频、文件传输等,以确保网络带宽能够按照需要进行分配。这种技术可以帮助组织实现更好的网络响应时间、更高的数据吞吐量和更好的用户体验。
另外,流量染色还可以用于防止网络攻击,例如 DDoS(分布式拒绝服务攻击),通过将针对特定目标的恶意流量限制或阻止,从而保护网络的可用性和稳定性。
二、介绍一下http和https的区别?为什么https安全?
HTTP(超文本传输协议)
和 HTTPS(安全超文本传输协议)
是用于在网络上传输数据的协议,它们之间的主要区别在于安全性和加密。
-
安全方面
Http
是一种用于传输超文本和网页内容的协议,在Web浏览器和Web服务器之间进行数据传输时使用,由于其传输是明文的不加密,因此数据在传输过程中容易被窃听、修改或伪造。
Https
利用了SSL/TLS协议
进行数据传输加密,提供了安全的网络通行。通过加密传输,可以保护数据不被窃听和篡改。使用Https的网站可以通过数字证书进行身份校验,确保数据发送到正确的服务器,避免了中间人攻击和网络钓鱼。 -
性能方面
Http协议的数据传输速度通常比HTTPS更快,因为HTTPS需要额外的加密解密过程来处理数据。 -
成本方面
部署HTTP比HTTPS更为简单、成本更低。HTTPS需要数字证书,而数字证书的获取和更新会带来一定的成本。 -
缓存方面
HTTP协议能够更容易在代理服务器或CDN中进行缓存,提高网站的性能和加载速度。而HTTPS传输的数据由于加密,不太适合缓存。 -
设备兼容性方面
一些较老的设备或系统对HTTPS支持可能不够友好,而HTTP则能够更广泛兼容。
HTTPS安全的原因
- 数据加密:HTTPS使用SSL/TLS协议对传输的数据进行加密,使得窃听者无法直接获取明文数据,确保数据传输的隐私和完整性。
- 身份认证:HTTPS使用数字证书对服务器和网站进行身份验证,确保通信双方的真实身份,防止中间人攻击。
- 完整性保护:HTTPS能够保护数据的完整性,一旦数据被篡改,通信双方都能够察觉到。
尽管HTTP具有以上一些优点,但需要注意的是,随着网络安全和隐私保护意识不断提高,现代网站普遍采用HTTPS来保证数据的安全和隐私。特别是对于处理用户敏感信息、进行金融交易、账号登录等操作的网站,使用HTTPS是非常重要的。因此,大多数情况下,选择HTTPS能够提供更好的安全性和用户信任度。
三、https的加密流程
HTTPS的加密流程可以简单地分为几个步骤:
-
客户端发起HTTPS连接,这通常是通过URL中使用"https://"来实现的。
-
服务端的回应:服务端向客户端返回数字证书,证明自己的身份。数字证书通常由权威机构颁发,用于证明服务端的身份。
-
客户端收到服务端的证书后,会验证证书的有效性,包括检查证书的签发机构、有效期等信息。
-
客户端和服务端进行协商,选择一个对称的加密算法(如AES)和密钥交换算法(如RSA)。
-
生成对称密钥:客户端生成一个随机的对称密钥并用服务端的公钥(从证书中获取)进行加密,然后发送给服务端。
-
密钥交换:服务端收到客户端发送的密钥后,使用自己的私钥进行解密,获得对称密钥。
-
加密通信:客户端和服务端使用协商好的对称密钥进行加密和解密通信,保障数据传输的安全性。
整个过程中使用了非对称加密和对称加密相结合的方式,保障了HTTPS连接的安全性和隐私性。
四、DNS 解析的流程
DNS解析是域名转换为IP地址的过程,其流程如下:
- 浏览器缓存:首先,浏览器会在本地缓存中查找请求的域名对应的IP地址,以确定是否已经解析过该域名。
- 操作系统缓存:如果浏览器缓存中没有找到对应的IP地址,操作系统检查自己的缓存中是否保存了该域名的IP地址。
- 路由器缓存:如果操作系统缓存中也没有找到对应的IP地址,请求会转到路由器上检查是否有该域名的缓存信息。
- ISP DSN服务器:如果以上缓存中都没有找到对应的IP地址,请求会发送到用户所连接的ISP的DSN服务器上。
- 根域服务器:如果ISP DSN服务器没有找到对应的IP地址,它会向根域服务器发送请求,获取顶级域名服务器的地址。
- 顶级域名服务器:ISP DNS服务器向顶级域名服务器发送请求,获取二级域名服务器的地址。
- 二级域名服务器:ISP DNS服务器向二级域名服务器发送请求,获取域名对应的IP地址。
- 响应返回:ISP DNS服务器将获取到的IP地址返回给用户的操作系统。
- 本地DNS缓存:用户的操作系统会将获取到的IP地址保存到本地缓存中,以供未来的查询。
整个过程中,如果某级缓存或DNS服务器中找不到对应的IP地址,则会向上一级发起请求,直到最终获取到域名对应的IP地址。
五、说明Socket是什么?
Socket(套接字)是一种通信端点的抽象表示,它为网络通信提供了一种机制,使得运行在不同主机上的进程能够相互发送和接收数据。Socket是网络编程中最基本的构建块,它定义了一种方式,使得不同主机上的应用程序能够通过一个网络连接进行双向通信。
六、TCP和UDP的区别?
TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)是计算机网络中常用的传输层协议,用于在计算机之间进行数据通信。它们有许多区别,主要涉及可靠性、连接性、传输效率等方面。
TCP协议特点:
可靠性:TCP提供可靠的数据传输,通过序列号、确认应答、超时重传、错误校验等机制来确保数据的完整性和可靠性。
连接:TCP是面向连接的协议,通信双方需要先建立连接,进行数据传输后再释放连接。
数据顺序保证:TCP保证数据包按发送顺序到达接收端,并将乱序数据包重新排序。
流量控制和拥塞控制:TCP通过滑动窗口和慢启动等机制来控制传输速率,避免拥塞、丢包等问题。
UDP协议特点:
无连接:UDP是无连接的协议,通信双方在传输数据时无需建立和释放连接,因此传输速度较快。
不可靠:UDP不提供可靠的数据传输,可能会出现丢包、乱序等问题,需要在应用层自行处理。
高效:由于不具备TCP的可靠性、流量控制等机制,UDP的传输效率更高,适合对实时性要求较高的应用场景。
广播和多播:UDP支持向多个目的地址发送数据,适合用于广播和多播的场景。
在实际应用中,TCP通常用于对数据可靠性要求较高的场景,如文件传输、网页访问等;而UDP则常用于实时音视频传输、视频会议、在线游戏等对实时性要求较高的场景。
七、TCP3次握手,4次握手的具体过程
TCP三次握手建立连接:
-
第一次握手:客户端向服务器发送一个SYN(同步)报文,用来发起一个主动打开连接的操作。客户端进入SYN_SENT状态。
-
第二次握手:服务器收到客户端的SYN报文后,如果同意连接,则会发送一个SYN-ACK(同步确认)报文。服务器进入SYN_RCVD状态。
-
第三次握手:客户端收到服务器的SYN-ACK报文后,会发送一个ACK(确认)报文作为响应。客户端和服务器此时都进入ESTABLISHED状态,连接建立成功,可以开始数据传输。
TCP四次挥手断开连接:
-
第一次挥手:当客户端或服务器中的任一方完成数据传输后,发送一个FIN(结束)报文,用来发起一个主动关闭连接的操作。
-
第二次挥手:接收到FIN报文的一方发送一个ACK报文作为回应,确认收到了对方的FIN请求。
-
第三次挥手:如果接收方也完成了数据传输,那么它也会发送一个FIN报文给发起方。
-
第四次挥手:发起方接收到这个FIN报文后,发送一个ACK报文作为最后的回应。此时,连接完全关闭。
八、挥手过程中的TIME_WAIT状态
当一方完成四次挥手中的第三次挥手后,它会进入TIME_WAIT状态。这个状态持续2倍的最大报文段生命周期(2MSL),以确保网络中没有遗留的重复报文。处于TIME_WAIT状态的连接占用的资源不会被内核释放,所以作为服务器,在可能的情况下,尽量不要主动断开连接,以减少TIME_WAIT状态造成的资源浪费。如果在TIME_WAIT期间收到了来自对方的任何报文,这些报文会被丢弃。
TIME_WAIT状态的作用
当一方执行主动关闭连接操作(通过调用close())时,它会发送一个FIN报文给对端,表示想要关闭连接。当收到对方的ACK确认后,该端进入TIME_WAIT状态。这个状态的目的是确保:
- 网络上所有重复的报文段(因为网络延迟或重传)都被自然消亡,这样新的连接就不会收到旧的、重复的数据。
- 允许足够的时间让对端处理和确认最终的FIN报文,即使对端的确认报文丢失,主动关闭方也可以重新发送FIN。
2MSL(最大报文段生存时间):MSL是TCP规范中定义的一个时间参数,它定义了TCP报文段在网络中的最大生存时间。2MSL确保了即使在最坏的情况下,所有旧的报文段都会被网络处理掉,不会影响新的连接。
TIME_WAIT状态是TCP连接关闭过程中的一个重要环节,它确保了TCP连接的可靠性和数据传输的完整性。
九、网络模型有哪几层?
网络模型是用于描述网络通信体系结构的分层模型。主要有两种网络模型:OSI七层模型和TCP/IP四层模型。
1、OSI七层模型
开放式系统互联通信参考模型(Open Systems Interconnection Model,简称OSI模型)是一个理论上的七层网络模型,它定义了网络通信的标准和协议。OSI模型的七层如下:
-
物理层(Physical Layer):负责在物理媒介上传输原始的比特流。涉及电气信号、光信号、物理连接和硬件设备。
-
数据链路层(Data Link Layer):确保物理层传输的数据无误,通过帧的方式传输数据,并处理错误检测和纠正。
-
网络层(Network Layer):负责数据包从源到目的地的传输和路由选择。
-
传输层(Transport Layer):提供端到端的数据传输服务,确保数据的完整性和可靠性,主要协议有TCP和UDP。
-
会话层(Session Layer):管理和控制两个通信系统之间的会话连接。
-
表示层(Presentation Layer):处理数据的表示、编码和转换,确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
-
应用层(Application Layer):为应用软件提供网络服务,如HTTP、FTP、SMTP等网络服务。
2、TCP/IP四层模型
传输控制协议/互联网协议模型(Transmission Control Protocol/Internet Protocol Suite,简称TCP/IP模型)是一个实际应用中的四层网络模型,它是因特网的基础。TCP/IP模型的四层如下:
-
链路层(Link Layer):对应于OSI模型的物理层和数据链路层,负责在物理媒介上传输数据帧。
-
网络层(Internet Layer):主要协议是IP,负责数据包从源到宿的传输和路由选择。
-
传输层(Transport Layer):与OSI模型相同,提供端到端的数据传输服务,主要协议有TCP和UDP。
-
应用层(Application Layer):与OSI模型相同,为应用软件提供网络服务。
两种模型的比较
- OSI模型是一个理论上的模型,而TCP/IP模型是实际应用于因特网的标准。
- OSI模型有七层,而TCP/IP模型通常被描述为有四层,但实际上TCP/IP模型也隐含了类似于OSI模型下两层的内容,只是没有明确地将它们作为单独的层级划分出来。
- TCP/IP模型更加简洁,易于理解和实现,因此在实际的网络通信中得到了广泛的应用。
网络模型的每一层都负责不同的网络通信任务,并且每一层都使用下一层提供的服务,同时为上一层提供服务。这种分层的方法使得网络通信更加模块化,易于设计、实现和故障排除。
数据库编程篇
一、MySQL有哪些数据类型?
-
整数类型,包括
TINYINT
、SMALLINT
、MEDIUMINT
、INT
、BIGINT
,分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。 -
实数类型,包括FLOAT、DOUBLE、DECIMAL。
DECIMAL
可以用于存储比BIGINT还大的整数,能存储精确的小数。FLOAT和DOUBLE
是有取值范围的,并支持使用标准的浮点进行近似计算。计算时FLOAT和DOUBLE相比DECIMAL效率更高一些。DECIMAL
你可以理解成用字符串进行处理
-
字符串类型,包括
VARCHAR
、CHAR
、TEXT
、BLOB
。VARCHAR
用于存储可变长字符串,它比定长类型更节省空间。VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。VARCHAR存储的内容超出设置的长度时,内容会被截断。CHAR
是定长的,根据定义的字符串长度分配足够的空间。CHAR会根据需要使用空格进行填充方便比较。CHAR适合存储很短的字符串,或者所有值都接近同一个长度。CHAR存储的内容超出设置的长度时,内容同样会被截断。
使用策略:
- 对于经常变更的数据来说,CHAR比VARCHAR更好,因为CHAR不容易产生碎片。
- 对于非常短的列,CHAR比VARCHAR在存储空间上更有效率。
- 使用时要注意只分配需要的空间,更长的列排序时会消耗更多内存。
- 尽量避免使用TEXT/BLOB类型,查询时会使用临时表,导致严重的性能开销。
-
枚举类型,把不重复的数据存储为一个预定义的集合。有时可以使用
ENUM
代替常用的字符串类型。ENUM存储非常紧凑,会把列表值压缩到一个或两个字节。ENUM在内部存储时,其实存的是整数。尽量避免使用数字作为ENUM枚举的常量,因为容易混乱。排序是按照内部存储的整数。 -
日期和时间类型,尽量使用
timestamp
,空间效率高于datetime,用整数保存时间通常不方便处理。如果需要存储微妙,可以使用BIGINT存储。
二、什么是索引?
- 索引是一种能提高数据库查询效率的数据结构。它可以比作一本字典的目录,可以帮你快速找到对应的记录。
- 索引一般存储在磁盘的文件中,它是占用物理空间的。
- 正所谓水能载舟,也能覆舟。适当的索引能提高查询效率,过多的索引会影响数据库表的插入和更新功能。
三、MySQL索引有哪些类型?
数据结构维度:
- B+树索引:所有数据存储在叶子节点,复杂度为O(logn),适合范围查询。
- HASH(哈希索引): 哈希索引仅支持精确匹配和前缀匹配,以及对某些操作如COUNT(DISTINCT…)的优化。适合等值查询,检索效率高,一次到位。
- FULLTEXT(全文索引):MyISAM和InnoDB中都支持使用全文索引,一般在文本类型char,text,varchar类型上创建。
- R-Tree索引: 用来对GIS数据类型创建SPATIAL索引
物理存储维度:
- CLUSTERED(聚簇索引):聚集索引就是以主键创建的索引,在叶子节点存储的是表中的数据。在MySQL中,主键默认是聚簇索引,如果没有主键,则会选择一个唯一的非空索引作为聚簇索引。
- NON-CLUSTERED(非聚簇索引):非聚集索引就是以非主键创建的索引,在叶子节点存储的是主键和索引列。
逻辑维度:
- PRIMARY KEY(主键索引):一种特殊的唯一索引,不允许有空值。
- INDEX(普通索引):MySQL中基本索引类型,允许空值和重复值。
- FOREIGN KEY(外键索引):用于引用另一个表中的主键,以维护两个表之间的数据一致性和引用完整性。
- COMPOSITE INDEX(复合索引):多个字段创建的索引,使用时遵循最左前缀原则。
- SECONDARY KEY / UNIQUE(唯一索引):索引列中的值必须是唯一的,但是允许为空值。
- SPATIAL(空间索引):MySQL5.7之后支持空间索引,在空间索引这方面遵循OpenGIS几何数据模型规则。用于地理空间数据类型,如GEOMETRY。
四、索引有哪些优缺点?
优点:
- 索引可以加快数据查询速度,减少查询时间。
- 唯一索引可以保证数据库表中每一行的数据的唯一性。
缺点:
- 创建索引和维护索引要耗费时间。
- 索引需要占物理空间,除了数据表占用数据空间之外,每一个索引还要占用一定的物理空间。
- 以表中的数据进行增、删、改的时候,索引也要动态的维护。
五、创建索引的三种方式?
- 在执行CREATE TABLE时创建索引
CREATE TABLE user_index2 (
id INT auto_increment PRIMARY KEY,
first_name VARCHAR (16),
last_name VARCHAR (16),
id_card VARCHAR (18),
information text,
KEY name (first_name, last_name),
FULLTEXT KEY (information),
UNIQUE KEY (id_card)
);
- 使用ALTER TABLE命令去添加索引
ALTER TABLE table_name ADD INDEX index_name (column_list);
- 使用CREATE INDEX命令创建
CREATE INDEX index_name ON table_name (column_list);
六、大表如何添加索引
为大型表添加索引是一个需要谨慎处理的任务,因为不当的索引可能会降低性能,而不是提高它。可以参考以下方法:
1、确定合适的列
- 高选择性:选择那些具有高选择性的列,即列中唯一值与总行数的比例较高的列。
- 频繁查询:确定哪些列经常用于查询条件(WHERE子句)、排序(ORDER BY子句)或连接条件(JOIN子句)。
2、使用索引分析工具
- 查询分析:使用数据库的查询分析工具来识别慢查询,并确定它们是否可以从索引中受益。
- 索引推荐:一些数据库管理系统(DBMS)可以推荐索引,基于查询模式和表的使用情况。
3、索引类型选择
- B-Tree索引:最常见的索引类型,适用于全值和范围查询。
- 哈希索引:适用于等值查询。
- 全文索引:适用于文本搜索。
- 空间索引:适用于地理空间数据。
4、如果查询经常涉及多个列,考虑创建一个包含这些列的复合索引。
5、索引维护
- 监控性能:在添加索引后,监控数据库性能,确保索引提供了所需的性能提升。
- 碎片整理:随着时间的推移,索引可能会变得碎片化,需要定期维护。
6、对于非常大的表,可以考虑使用分区来提高查询性能和索引管理。
7、尽可能在系统负载较低的时候添加索引,以减少对生产环境的影响。
8、不要一次性为大量列添加索引,逐步添加并评估每个索引的效果。
需要注意的是,索引可以加速读操作,但会稍微减慢写操作(INSERT、UPDATE、DELETE),因为索引本身也需要更新。在为大型表添加索引时,始终要权衡索引带来的好处和它们对维护、存储以及写操作性能的影响。务必在实施之前进行彻底的测试。
七、索引什么时候会失效?
-
查询条件包含OR:如果查询条件中使用了
OR
,并且不是所有条件都使用索引列,索引会失效。 -
字符串类型字段不加引号:在某些数据库中,字符串类型字段在不加引号的情况下参与比较时,可能会进行隐式类型转换,导致索引失效。
-
LIKE通配符:使用
LIKE
操作符时,尤其是当通配符%
或_
位于字符串的开头时,索引会失效。 -
联合索引非最左前列:在使用联合索引时,如果查询条件没有从联合索引的最左侧列开始,索引会失效。
-
索引列上使用内置函数:在索引列上使用函数进行操作,如
CONCAT()
或DATE()
,索引可能不会生效。 -
对索引列运算:对索引列进行算术运算,如加法或乘法,索引可能不会生效。
-
使用!= 或者 < >,NOT IN:使用不等于(
!=
)或范围查询(< >
),以及NOT IN
,可能导致索引失效。 -
使用IS NULL,IS NOT NULL:虽然这些操作符通常不会使索引失效,但某些数据库系统或特定情况下,优化器可能决定不使用索引。
-
不同编码格式的字段连接查询:如果连接查询中的字段编码格式不一致,这可能导致数据库无法有效使用索引。
-
MySQL估计全表扫描更快:优化器基于统计信息和成本估算,可能会决定使用全表扫描而不是索引。
八、哪些场景不适合建立索引?
虽然索引可以提高查询性能,但并不是所有场景都适合建立索引。以下是一些不适合建立索引的情况:
-
低基数列:如果列中含有大量重复值(即低基数),索引可能不会提供显著的性能提升。
-
频繁更新的列:如果列经常发生变化(INSERT、UPDATE、DELETE),维护索引的成本可能会超过查询优化的收益。
-
小表全表扫描:对于小表,全表扫描通常比索引查找更快,因为索引需要额外的查找和排序开销。
-
很少使用的列:如果列很少作为查询条件,为它们建立索引可能不会带来性能上的提升。
-
宽索引:如果索引包含多个列,并且这些列的数据类型都是比较宽的(例如,文本或二进制数据),索引可能会占用大量空间并降低性能。
-
排序规则不一致:如果列的排序规则(collation)与将要使用的查询条件的排序规则不一致,索引可能不会生效。
-
使用特殊数据类型:某些特殊数据类型或编码格式的列可能不适合建立索引,因为它们难以高效地进行索引和搜索。
-
索引选择性不高:如果索引的选择性不高,即索引列中的唯一值与总行数的比例较低,索引的效果可能受限。
-
过度索引:已经有冗余的索引的情况(比如已经有a,b的联合索引,不需要再单独建立a索引),为表中的每个列都建立索引,尤其是复合索引,可能会导致索引过多,增加维护成本并降低写操作性能。
-
动态列:如果列的值经常变化,或者列的值是由其他列的值动态计算得出的,为这些列建立索引可能不经济。
-
分区表的非分区键列:对于分区表,如果查询经常针对分区键进行,那么为非分区键列建立索引可能不会带来性能上的提升。
-
临时表或结果集:对于临时使用的表或查询结果集,建立索引可能没有必要,因为它们只会短暂存在。
-
非等值查询:如果查询主要涉及非等值条件(如
>
、<
),索引可能不会提供预期的性能提升。
九、索引下推了解过吗?什么是索引下推
索引下推(ICP) 是一种数据库查询优化技术,它允许数据库引擎将一部分查询条件的评估推迟到索引查找过程中。这意味着查询条件可以在索引遍历的过程中被应用,从而减少需要扫描的索引条目数量,进而减少访问表数据所需的I/O操作。
示例,假设name和age定义了联合索引idx_name_age,有SQL如下:
select * from employee where name like '小%' and age=28 and sex='0';
在MySQL5.6版本之前,当通过索引树idx_name_age
找出所有名字第一个字是“小”的人,获取到主键ID后,需要回表找出数据行,再去对比年龄和性别等字段。
有些朋友可能觉得奇怪,idx_name_age
(name,age)不是联合索引嘛?为什么选出包含“小”字后,不再顺便看下年龄age再回表呢,不是更高效嘛?所以呀,MySQL 5.6就引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
因此,MySQL5.6版本之后,选出包含“小”字后,顺表过滤age=28,然后再回表查询sex='0’的条件。
十、聚簇索引与非聚簇索引的区别
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。聚簇索引中索引和数据行是一起存储的,而非聚簇索引中索引和数据行是分开的。
从MySQL的存储引擎出发,聊聚簇索引和非聚簇索引的区别,可以有更加清晰的认知:
针对InnoDB和MyISAM这两种MySQL存储引擎,聚簇索引和非聚簇索引的实现确实存在明显差异。
InnoDB存储引擎:
- InnoDB的聚簇索引通常是基于主键构建的。在聚簇索引中,非索引列(即非主键列)的值与索引列一起存放,这意味着索引的叶节点存储了一整行记录。
- InnoDB的非聚簇索引(也称为二级索引)叶子节点存储的是索引列和主键值。因此,一般非聚簇索引还需要回表查询。通常需要两个步骤:首先在非聚簇索引中找到主键值,然后使用主键值在聚簇索引中查找完整的行记录,这个过程称为“回表”。
- 一个表中只能拥有一个聚集索引(因为一般聚簇索引就是主键索引),而非聚集索引一个表则可以存在多个。
- 一般来说,相对于非聚簇索引,聚簇索引查询效率更高,因为不用回表。
MyISAM存储引擎:
- MyISAM存储引擎使用非聚簇索引。无论主键索引还是普通索引,它们的叶节点都只包含索引数据和指向表数据的地址(行指针)。
- 在MyISAM中,数据和索引是分开的,叶子节点都使用一个地址指向真正的表数据。
- 由于MyISAM使用非聚簇索引,查找数据时也需要通过索引中的指针回表查询对应的数据行。
十一、什么是覆盖索引?
覆盖索引(Covering Index)是一种数据库索引技术,它使得查询所需的所有数据都可以从索引本身获取,无需访问主表。覆盖索引通常包括了查询中涉及的所有字段,以及需要排序或返回的字段。换句话说,查询列要被所建的索引覆盖。
使用覆盖索引可以显著提高查询性能,因为数据库引擎可以直接从索引中读取数据,避免了对主表的访问,减少了I/O操作。也减少了对表数据的访问,这在数据量很大的情况下尤其有用。如果索引已经包含了排序所需的列,那么使用覆盖索引可以避免额外的排序操作。
示例
SELECT column1, column2 FROM table WHERE column3 = 'aaa';
为了创建覆盖索引,可以这样定义索引:
CREATE INDEX index_name ON table(column3, column1, column2);
在这个例子中,index_name
是一个覆盖索引,它包括了查询条件中的column3
,以及选择列表中的column1
和column2
。
不过值得注意的是,覆盖索引可能会比非覆盖索引更大,因为它包含了更多的列。且由于覆盖索引包含更多列,维护这些索引(如插入、更新、删除操作)的成本可能会更高,会消耗更多的磁盘空间。
十二、什么是回表?如何减少回表?
回表通常指的是在数据库查询中所发生的一种额外查询操作,即主要查询操作返回的结果需要再次访问表来获取完整的数据,因此称为“回表”。比如使用非聚簇索引(或称为二级索引)查找数据时,首先在索引中找到所需的键值,然后根据这些键值去主表中检索对应的行记录的过程。因为非聚簇索引的叶节点通常不包含非索引列的具体数据,而是存储指向主表中数据行的引用(如行ID),所以需要通过这个引用回到主表去获取完整的数据行。
示例,假设age定义了索引idx_age
,有SQL如下:
select * from employee where age=32;
这里需要查询所有列的数据,而idx_age
普通索引不能满足,需要拿到主键id的值后,再回到id主键索引查找获取,这个过程就是回表。
减少回表的方法:
-
使用覆盖索引:创建一个覆盖索引,包含查询所需的所有列,这样数据库引擎可以直接从索引中获取所有需要的数据,无需回表。
-
减少索引列:在非覆盖索引中,尽量减少索引中包含的列数,特别是那些查询中不需要的列,以减少索引的体积和维护成本。
-
优化查询条件:确保查询条件能够有效地使用索引,避免使用导致索引失效的条件,如使用函数或表达式索引列。
-
使用聚簇索引:在支持聚簇索引的存储引擎中(如InnoDB),确保经常一起访问的列被包含在聚簇索引中,因为聚簇索引的叶节点包含了完整的数据行。
-
索引选择性:选择具有高选择性的列作为索引列,这样可以减少索引中的条目数量,提高索引查找效率。
-
索引合并:在某些数据库系统中,优化器可能会使用索引合并技术,结合多个索引来满足查询需求,减少回表次数。
-
数据库配置:根据所使用的数据库系统,调整相关配置,以优化索引的使用和查询性能。
-
查询重写:重写查询语句,使其更加高效,减少不必要的列访问和数据行的回表。
-
分析和监控:定期分析查询性能和索引使用情况,监控慢查询日志,找出需要优化的地方。
通过上述方法,可以在一定程度上减少回表操作,提高数据库查询的性能。然而,需要注意的是,减少回表操作可能会与索引的设计和维护成本之间存在权衡,因此在实际应用中需要综合考虑。
十三、B+树的底层是什么
B+树是一种自平衡的树型数据结构,通常用于数据库和文件系统中,用来存储有序的数据和提供快速的检索。B+树相对于其他树型数据结构,如二叉搜索树和平衡二叉树,具有更高的磁盘IO效率,因此在大规模存储和检索数据时被广泛使用。
B+树的底层结构设计是为了优化磁盘IO操作。在磁盘存储中,数据的读写通常是以块(block)为单位进行的,而每个块的大小是有限的。通过利用这一结构特点,B+树将数据结构的设计和存储方式紧密结合,使得每次读取的数据尽可能多且更加紧凑,从而减少IO次数,提升检索效率。
十四、B+树有什么特点
- 所有关键字都在叶子结点上,非叶子结点仅用来索引。
- 叶子结点之间通过指针相互连接,形成有序的链表结构,便于范围查询和排序。
- 每个结点存储的关键字数量相对较多,可以充分利用磁盘块的大小,减少IO次数。
- B+树通过优化磁盘IO操作,使得在大规模数据存储和检索场景下具有较高的效率和性能。在数据库和文件系统中,这种特性非常适合大规模数据的存储和索引需求,因此被广泛应用。
十五、为什么要用 B+树,为什么不用二叉树或者其他树形结构?
对于这种问题,可以从几个维度去思考问题的本质,从而找到区别点和扩展点:查询是否够快?效率是否稳定?存储数据多少? 以及查找磁盘次数?为什么不是二叉树?为什么不是平衡二叉树?为什么不是 B 树,而偏偏是 B+树呢?
-
查询效率:
- 二叉树:在最坏的情况下,二叉树可能退化成链表,导致查询效率降低到O(n)。
- 平衡二叉树:虽然提供了稳定的O(log n)查找效率,但每个节点只存储一个键值和数据,不适合磁盘存储。
-
效率稳定性:
- 二叉树:效率不稳定,容易退化。
- 平衡二叉树:效率稳定,但在磁盘I/O方面不如B树和B+树。
-
存储数据量:
- B树和B+树:每个节点可以存储更多的键值,适合存储大量数据。
-
查找磁盘次数:
- B树:节点可以存储多个键值和数据,降低了树的高度,减少了查找时的磁盘I/O次数。
- B+树:由于非叶子节点不存储数据,可以存储更多的键值,进一步降低树的高度,减少I/O次数。
-
为什么不是平衡二叉树:
- 平衡二叉树每个节点只存储一个键值,导致树高,增加了磁盘I/O次数,不适合作为数据库索引。
-
为什么是B+树而不是B树:
- 节点存储:B+树的非叶子节点仅存储键值,不存储数据,使得每个节点可以存储更多的键值,树更矮胖。
- 磁盘I/O:B+树的结构减少了磁盘I/O次数,提高查询效率。
- 范围查询:B+树的叶子节点存储所有数据,并形成有序链表,非常适合执行范围查询和顺序访问。
-
为什么不是一般二叉树:
- 一般二叉树无法保证平衡,容易退化成链表,失去索引效率。
-
为什么B+树优于B树:
- B+树的所有数据都存储在叶子节点,并且叶子节点之间通过指针连接,这使得范围查询和顺序访问更加高效。
- B+树的结构使得每个节点可以存储更多的键值,进一步降低树高,减少磁盘I/O。
总结来说,B+树在数据库索引中的选择是因为它在查询效率、效率稳定性、存储数据量、查找磁盘次数等方面都提供了优化,特别是在减少磁盘I/O次数和支持范围查询方面的优势,使其成为数据库索引的理想选择。
十六、Hash 索引和 B+树区别是什么?你在设计索引是怎么抉择的?
当比较Hash索引和B+树索引的区别以及在设计索引时如何做出抉择时,我们可以从以下几个关键维度进行考虑:
Hash索引与B+树索引的区别:
-
范围查询:
- B+树:由于其有序性质,非常适合进行范围查询,可以快速定位到范围的起点并顺序访问。
- Hash索引:不支持范围查询,因为哈希表中的元素是无序的。
-
联合索引的最左侧原则:
- B+树:在联合索引中,查询必须从最左侧的列开始,这样才能有效利用索引。
- Hash索引:通常不支持联合索引的最左侧原则,因为它们不保持列的顺序。
-
ORDER BY排序:
- B+树:由于数据的有序性,B+树可以很高效地支持ORDER BY排序。
- Hash索引:不支持ORDER BY排序,因为索引本身不存储元素的顺序。
-
等值查询效率:
- Hash索引:在没有哈希冲突的情况下,等值查询可以非常快速,接近O(1)的时间复杂度。
- B+树:等值查询也很高效,但通常略逊于Hash索引,特别是在索引列值唯一性很高的情况下。
-
处理哈希冲突:
- Hash索引:当出现大量重复值时,哈希冲突会增加,需要通过链表或其他方法解决冲突,这会降低效率。
- B+树:不存在冲突问题,因为每个键在树中都有固定的位置。
-
模糊查询和LIKE操作:
- B+树:可以使用LIKE操作进行模糊查询,尤其是当查询模式以非通配符开头时,可以利用索引进行优化。
- Hash索引:由于不存储顺序,通常无法用于模糊查询或LIKE操作的优化。
-
数据分布:
- Hash索引:可能会因为哈希冲突导致数据分布不均匀,影响查询性能。
- B+树:数据分布更加均匀,因为树的每个节点都平衡。
-
空间效率:
- B+树:通常在存储大型数据集时更有效,因为树结构可以很好地扩展。
- Hash索引:可能需要额外的空间来解决哈希冲突。
在设计索引时,需要根据实际的查询模式、数据特性和性能要求来选择最合适的索引类型。B+树索引因其多方面的适用性和效率,在数据库系统中被广泛使用,尤其是在需要处理大量数据和复杂查询的场景中。而Hash索引则在某些特定场景下(如高频等值查询且索引列值唯一性高)表现出优势。
设计索引时的抉择:
-
查询类型:
- 如果应用主要进行等值查询,可以考虑Hash索引。
- 如果需要频繁进行范围查询和排序,B+树索引是更好的选择。
-
数据特性:
- 对于有大量重复键值的数据集,Hash索引可能更有效。
- 对于需要保持数据有序性的场合,B+树索引更合适。
-
存储引擎特性:
- 某些存储引擎可能对某种类型的索引有优化,需要根据实际使用的存储引擎特性来选择索引类型。
-
写操作的频率:
- 如果应用中写操作(插入、删除、更新)非常频繁,需要考虑索引的维护成本,B+树索引可能更优。
-
并发控制:
- B+树索引通常提供更好的并发性能,因为它可以锁定较小的数据范围。
-
硬件和性能要求:
- 考虑内存大小、磁盘I/O性能等因素,选择最适合当前硬件和性能要求的索引类型。
-
特定功能需求:
- 如果需要支持事务、外键等数据库功能,某些类型的索引可能更合适。
综上所述,选择Hash索引还是B+树索引,需要根据具体的查询需求、数据特性、存储引擎特性以及应用场景来综合考虑。通常,B+树索引由于其多方面的优势,在数据库系统中更为常用,尤其是在需要处理大量数据和高并发访问的场合。
十七、数据库三大范式
数据库的三大范式是一组用于指导数据库设计的规则,旨在减少数据冗余和提高数据完整性。
-
第一范式(1NF) - 列不可再分
- 要求数据库表的每一列都是不可分割的基本数据项,即表中的所有字段都应该只包含原子性的、单一的数据点,而不能包含集合、数组或对象等。
- 确保每个字段都只包含单一值,这样有助于避免数据重复。
-
第二范式(2NF) - 行定义唯一区分,即主键约束
- 在满足1NF的基础上,要求表中的每一行都应可唯一标识,即表中不存在仅依赖于表中的一部分列的非主属性(即非平凡且非函数依赖的属性)。
- 换句话说,所有非主键属性必须完全依赖于主键,以消除冗余并保证数据的一致性。
-
第三范式(3NF) - 非主键属性不可与其他表的非主属性关联,即外键约束
- 在满足2NF的基础上,要求非主键列之间不能相互依赖,即不能存在这样的情况:一个非主键属性依赖于另一个非主键属性。
- 这意味着表中的每一列都应直接依赖于主键,而不是通过另一个非主键列间接依赖。
除了这三大范式外,还有其他的范式,如BCNF(巴斯-科德范式)、第四范式(4NF)和第五范式(5NF),它们提供了更严格的数据规范化要求。
-
BCNF(巴斯-科德范式):是3NF的加强版,要求任何非平凡的函数依赖的属性集合都不能是其他函数依赖的子集。
-
第四范式(4NF) - 多值依赖:
- 要求数据库表不应存在多值依赖,即一个表中不应该有两个或多个独立的多值事实关于同一个主键。
-
第五范式(5NF) - 连接依赖:
- 要求表中不应该存在连接依赖,即不应该通过多个步骤的连接操作来获取数据。
遵循这些范式可以帮助设计出结构良好、数据冗余最小化的数据库。然而,实际应用中,为了提高性能,有时会有意违反某些范式,进行所谓的反规范化。反规范化通过增加一些冗余来减少复杂的查询,提高读写性能。
十八、MySQL常用的数据库引擎有哪些?
数据库引擎,也常被称为存储引擎或数据库管理系统(DBMS)的核心组件,负责数据的存储、索引和检索。不同的数据库引擎提供了不同的功能和优化,以适应不同的使用场景。
-
MyISAM:
- 使用全表锁,适合读密集型的应用。
- 由于不支持事务和外键,适用于对数据完整性要求不高的场景。
- 占用空间相对较小,但在高并发写入的场景下性能较差。
- 以select、insert为主的应用基本上可以使用这引擎
-
InnoDB:
- 使用行级锁和 MVCC(多版本并发控制),提供了高并发写入的能力。
- 支持事务、外键约束,以及崩溃恢复能力,适合需要事务完整性的应用。
- 占用空间是MYISAM的2.5倍,存储空间和性能可能不如MyISAM,但在多数情况下提供了更好的数据安全性和并发控制。
-
Memory:
- 使用全表锁,数据存储在内存中,访问速度快。
- 数据在数据库重启时会丢失,适合临时数据或会话数据存储。
- 默认使用HASH索引,适合快速查找和读取,但不适合大规模数据存储,主要用于那些内容变化不频繁的代码表。
-
Merge:
- 是一种表的类型,也称为合并表或联合表。
- 由一组具有相同结构的MyISAM表组成,MySQL通过一个合并层来统一访问这些表,就像它们是一个单独的表一样。
- Merge表主要用于将多个小表逻辑上组合成一个大表,以提高查询性能
-
Archive:
- 用于存储大量未修改的数据,如日志信息。
- 支持高压缩比,节省存储空间,但只支持INSERT和SELECT操作。
-
Federated:
- 允许访问远程MySQL服务器上的表,表实际上是存储在远程服务器上的。
-
CSV:
- 允许MySQL读取和写入逗号分隔值(CSV)文件。
十九、如何选择引擎?
- 如果没有特别的需求,使用默认的Innodb即可。
- 如果应用需要事务支持(包括提交、回滚和崩溃恢复能力),应选择支持事务的引擎,如InnoDB。
- 对于需要高并发写入的应用,选择支持行级锁的引擎(如InnoDB)通常更合适
- 考虑存储需求和预算,选择存储效率高的引擎。例如,InnoDB可能比MyISAM占用更多空间。
- 如果应用主要是读取操作,可以考虑MyISAM,但对于高写入负载,InnoDB或Memory可能更合适。
- 如果服务器有足够的内存,并且需要快速访问数据,可以考虑使用Memory引擎。
- 如果数据需要在数据库重启后仍然可用,应避免使用Memory引擎,因为它的数据在重启后会丢失。
- 考虑查询模式和需要的索引类型。例如,如果需要全文索引,可以考虑MyISAM或InnoDB(InnoDB从MySQL 5.6.4开始支持全文索引)。
二十、Mysql的锁有哪些?
按照对数据操作的锁粒度来分,有行级锁
、表级锁
、页级索
、间隙锁
、临键锁
。
-
行级锁:
- 是MySQL中锁定粒度最细,加锁开销最大,并发度最高的一种锁.。适用于高并发场景,因为它允许其他事务访问同一表中的其他行。
- 行级锁分为共享锁和排他锁。其中行级锁和页级索之间还有其他锁粒度的锁,即间隙锁(Gap Lock)和临键锁(Next-Key-Lock)。
- InnoDB存储引擎支持行级锁,它通过索引来确定哪些行需要被锁定。
-
表级锁:
- 是MySQL中锁定粒度最大,加锁快,不会出现死锁,并发度最低的一种锁。锁定整张表,适用于写操作较少,读操作非常频繁的场景。
- MyISAM和Memory存储引擎使用表级锁。
-
页级锁:
- 是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。锁定数据库中的一页,通常为16KB或更大的数据块。
- BDB存储引擎支持页级锁,但InnoDB不支持。InnoDB通常使用行级锁,但在某些情况下,如对非索引列进行查询时,可能会使用页级锁。
-
间隙锁(Gap Lock):
- 锁定索引记录中的一个间隙,而不是具体的索引记录。它用于防止新的记录在间隙中插入。
- 常用于防止幻读。
-
临键锁(Next-Key Lock):
- 是行级锁的一种,结合了记录锁和间隙锁,用于锁定一个记录以及记录前面的间隙。
- 用于处理范围查询并防止幻读。
按照锁的共享策略来分,有共享锁
、排他锁
、意向共享锁
、意向排他锁
。
-
共享锁(Shared Lock, S锁):
- 读锁,也叫共享锁,S锁。
- 允许持有锁的事务读取数据。
- 多个事务可以同时持有同一数据的共享锁。
-
排他锁(Exclusive Lock, X锁):
- 写锁,也叫排他锁,X锁。
- 允许持有锁的事务修改数据。
- 同一数据同时只能被一个事务持有排他锁。
-
意向共享锁(Intention Shared Lock, IS锁):
- IS锁,又称意向共享锁。
- 表明事务即将对更低一级的粒度(如行级)加共享锁。
- 用于在锁定层次结构中向上逐级表示锁定意图。
-
意向排他锁(Intention Exclusive Lock, IX锁):
- IX锁,又称意向排他锁。
- 表明事务即将对更低一级的粒度加排他锁。
- 同样用于表示锁定意图。
补充
- 共享锁之间是兼容的,多个事务可以同时持有同一数据的共享锁。排他锁与其他类型的锁不兼容。
- 行级锁和间隙锁可能导致死锁,因为多个事务可能会尝试以不同的顺序获取锁。
- 在某些存储引擎中,锁可以从更细粒度的锁升级到更粗粒度的锁,例如从行级锁升级到表级锁。
- InnoDB支持行级锁和表级锁,默认为行级锁,但在全表扫描时可能会使用表级锁。MyISAM和Memory采用表级锁。BDB采用页级锁或表级锁。
二一、InnoDB三种行锁的算法是什么?
Record Lock(记录锁):
- 记录锁,也称为行锁,是最常用的锁类型之一。
- 当事务要修改一条具体的记录时,InnoDB会在这条记录上加上记录锁。
- 记录锁只锁定符合条件的行,不影响其他行。
Gap Lock(间隙锁):
- 间隙锁用于锁定一个范围,但不包括记录本身。
- 它用于防止新的行插入到被锁定的范围内,从而防止幻读现象。
- 间隙锁通常用在
SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
语句中,当查询的条件涉及索引间隙时。
Next-Key Lock(临键锁):
- 临键锁是InnoDB中的默认行锁算法,它是记录锁和间隙锁的组合。
- 临键锁不仅锁定一个具体的记录,还锁定记录前面的间隙。
- 当一个事务在索引记录上加上临键锁时,它会锁定该记录以及该记录之前的间隙,但不包括后面的间隙。假设有记录1, 3, 5, 7,现在记录5上加next-key lock,则会锁定区间(3, 5],任何试图插入到这个区间的记录都会阻塞。
- 临键锁可以防止其他事务在锁定范围内插入新记录,并且可以防止幻读。
二二、mysql开发中有没有遇到过死锁的情况
当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据,造成互相等待的情况,若无外力作用,它们都将无法推下去,此时称系统处于死锁状态或系统产生了死锁。不过表级锁不会产生死锁,所以解决死锁主要还是针对于最常用的Innodb。
在开发中,有过一个场景,用户A对两份数据执行操作,用户B也对这两份数据同时执行操作,但两个用户对这两份数据的执行顺序不同,导致了加锁的顺序也不一样,这样就出现了死锁现象。当然,改进这个问题,只需要将两份数据直接一次锁住即可。
还有开发中经常会根据字段值查询,如果不存在,则插入,否则更新。当对存在的行进行锁的时候,MySQL就只有行锁,当对未存在的行进行锁的时候,即使条件为主键,MySQL还是会锁住一段范围(gap锁)。锁住的范围为:无穷小或小于表中锁住id的最大值,无穷大或大于表中锁住id的最小值。对付这种死锁问题,可以使用insert into 表名 on duplicate key update ‘xx’='XX’语句即可,因为insert语句对于主键来说,插入的行不管有没有存在,都只会有行锁。
还有如果两个session分别通过一个SQL持有一把锁,然后互相访问对方加锁的数据,也会产生死锁。
补充
如果遇到存在高并发并且对于数据的准确性很有要求的场景,是要了解和使用for update的。比如涉及到金钱、库存等,一般这些操作都是很长一串并且是开启事务的。所以需要for update进行数据加锁防止高并发时候数据出错。
分布式系统篇
一、什么是分布式事务?
分布式事务
跟数据库事务有点不一样,它是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说,分布式事务指的就是分布式系统中的事务,它的存在就是为了保证不同数据库节点的数据一致性。
分布式事务基础
分布式事务需要需要知道CAP理论和BASE理论。
- CAP理论
- 一致性(C:Consistency): 一致性是指数据在多个副本之间能否保持一致的特性。例如一个数据在某个分区节点更新之后,在其他分区节点读出来的数据也是更新之后的数据。
- 可用性(A:Availability): 可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是"有限时间内"和"返回结果"。
- 分区容错性(P:Partition tolerance): :分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务。
一个分布式系统中,CAP理论它只能同时满足(一致性、可用性、分区容错性)中的两点。
- BASE 理论
BASE 理论, 是对CAP中AP的一个扩展,对于我们的业务系统,我们考虑牺牲一致性来换取系统的可用性和分区容错性。BASE是Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。
- 基本可用是指,通过支持局部故障而不是系统全局故障来实现的;
- Soft State表示状态可以有一段时间不同步;
- 最终一致,最终数据是一致的就可以了,而不是实时保持强一致。
二、分布式事务的几种解决方案
分布式事务是分布式系统中保证数据一致性的关键技术,以下是一些常见的分布式事务解决方案:
-
两阶段提交(2PC):
- 这是最传统的分布式事务解决方案,将事务的提交过程分为两个阶段:
准备阶段CanCommit
和提交阶段DoCommit
。在准备阶段,事务协调者询问所有参与者是否可以提交事务;在提交阶段,如果所有参与者都同意,则协调者通知它们正式提交事务。
- 这是最传统的分布式事务解决方案,将事务的提交过程分为两个阶段:
-
三阶段提交(3PC):
- 3PC是2PC的改进版本,增加了一个
预提交阶段PreCommit
,事务等到所有的参与者都响应Yes后,会向参与者发送PreCommit请求,并等待参与者的响应。主要解决了2PC中的单点故障问题。
- 3PC是2PC的改进版本,增加了一个
-
补偿事务(TCC):
- TCC的针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC模型将事务的执行分为
Try、Confirm和Cancel三个阶段
,Try阶段主要是对业务系统的检查和预留资源,Confirm阶段是对业务系统的最终提交,Cancel阶段则是在事务执行失败时对业务系统进行补偿(回滚)。
- TCC的针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC模型将事务的执行分为
-
可靠消息最终一致性方案:
- 这种方案通常使用消息队列来保证消息的可靠传递,通过本地消息表和消息中间件来实现事务的最终一致性。
-
最大努力通知:
- 这是一种轻量级的分布式事务解决方案,适用于对数据一致性要求不是特别高的场景。事务的发起方会尽最大努力通知接收方,但如果通知失败,接收方需要能够感知到并采取相应的措施。可以采用MQ的ACK机制。
-
本地消息表:
- 本地消息表的核心思想就是将分布式事务拆分成本地事务进行处理。在本地数据库中建立消息表,通过在本地事务中写入业务数据和消息数据,然后通过异步的方式发送消息,以此来保证最终一致性。
-
Seata AT模式:
- Seata的核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。是一款开源的分布式事务解决方案,提供了AT、TCC、SAGA和XA事务模式,其中AT模式通过记录业务数据的变更日志来实现分支事务的提交和回滚,适用于关系型数据库的分布式事务处理。
-
Seata TCC模式:
- 与AT模式类似,Seata也支持TCC模式,它通过用户自定义的二阶段提交逻辑来实现分布式事务。
-
Seata SAGA模式:
- SAGA模式适用于复杂的业务场景,它允许将一个长事务拆分成多个本地事务,每个本地事务都有相应的补偿操作,通过 Saga 工作流来管理这些事务,以保证数据的最终一致性。
-
BASE理论:
- BASE是Basically Available, Soft state, Eventual consistency的缩写,它与ACID相对,强调高可用性和动态的一致性,适用于分布式系统。
不同的业务场景和需求可能需要选择不同的分布式事务解决方案。在选择时,需要考虑系统的一致性要求、可用性、性能、复杂性等因素。
三、Spring框架怎么管理事务,用到什么原理?
所谓事务,是指逻辑单元内的一系列操作,要么全部完成执行,要么全部不执行。
Spring框架管理事务的核心原理是声明式事务管理,它允许在代码中以声明的方式配置事务的边界和属性,而不需要在业务逻辑中硬编码事务管理的代码。
Spring事务管理主要基于以下原理和组件:
-
事务属性的声明:
- 可以通过注解(如
@Transactional
)或XML配置在方法或类级别声明事务属性,如传播行为、隔离级别、超时设置等。
- 可以通过注解(如
-
事务的传播行为:
- 定义了当事务方法被其他方法调用时,事务如何被传播。Spring定义了多种传播行为,如
REQUIRED
、REQUIRES_NEW
、MANDATORY
等。
- 定义了当事务方法被其他方法调用时,事务如何被传播。Spring定义了多种传播行为,如
-
事务的隔离级别:
- 定义了事务在并发执行时的隔离程度,防止数据的脏读、不可重复读和幻读。Java事务API(JTA)和特定数据库的事务隔离级别可以被配置。
-
编程式事务管理:
- 通过
TransactionTemplate
或TransactionManager
编程式地管理事务,这种方式需要在代码中调用begin()
、Transaction()
、commit()
、rollback()
等事务管理Spring的相关方法,虽然不如声明式事务管理方便,但提供了更多的灵活性。
- 通过
-
声明式事务管理:
- 使用
@Transactional
注解是Spring中声明式事务管理的典型方式,它允许在方法或类级别声明事务的边界。
- 使用
-
Spring事务管理器(
TransactionManager
):- 它是Spring事务管理的核心接口,负责事务的开启、提交或回滚。Spring为不同的事务API(如JDBC、JTA、JPA等)提供了不同的事务管理器实现。
-
事务同步管理器(
TransactionSynchronizationManager
):- 它用于跟踪当前的事务状态,如事务是否激活、事务管理器是谁等。
-
事务定义信息(
TransactionDefinition
):- 它定义了事务的属性,如传播行为、隔离级别、超时时间等。
-
资源管理器:
- 如JDBC
DataSource
、JPAEntityManagerFactory
等,它们负责实际的数据访问和事务管理。
- 如JDBC
-
事务切面(
TransactionInterceptor
):- 在运行时,Spring会创建一个事务切面,它使用AOP(面向切面编程)将事务管理逻辑织入到业务逻辑中。
Spring事务管理器通常使用代理来实现。当我们调用一个声明了@Transactional
注解的方法时,实际上调用的是代理对象的方法。在代理对象的方法内部,Spring会根据事务属性创建或加入事务,并在方法执行前后适当地管理事务的提交和回滚。
Spring的事务管理是基于AOP的,它允许在不修改业务逻辑代码的前提下,通过配置来控制事务的边界和行为。这种声明式事务管理简化了事务管理的复杂性,使得开发人员可以更专注于业务逻辑的实现。
四、你知道哪几种声明式事务失效的场景吗?
声明式事务失效的场景有很多:
- 底层数据库引擎不支持事务,则Spring自然无法支持事务(无法支持声明式事务)。
- 在非public修饰的方法使用,@Transactional注解使用的是AOP,在使用动态代理的时候只能针对public方法进行代理,否则虽然不会抛出异常,但会导致事务失效
- 在整个事务的方法中使用try-catch,这会导致异常无法抛出,自然就导致了事务失效。
- 方法中调用同类的方法,简单来讲就是类中的A方法没有标注事务,但内部调用了标注声明式事务的B方法,这样会导致B方法中的事务失效。
五、Beanfactory和ApplicationContext有什么区别?
在Spring框架中,BeanFactory
和ApplicationContext
都是用于管理Spring应用中的bean生命周期的容器,但它们在功能和行为上有一些关键的区别:
- 定义方面:
BeanFactory
是Spring框架中的基础设施接口,提供了最基本的依赖注入功能。ApplicationContext
是BeanFactory
的子接口,它继承了BeanFactory
的所有功能,并添加了更多高级特性,是企业应用中常用的上下文类型。 - 加载方式:
BeanFactory
采用延迟加载的方式,只有在第一次请求的时候才会创建bean。而应用启动时,ApplicationContext
会立即加载所有的bean定义,并创建bean,因此可以及时发现配置错误。 - 单例管理:对于单例bean,
BeanFactory
默认是懒加载的,但可以通过BeanFactory
的preInstantiateSingletons()
方法来预初始化所有非懒加载的单例bean。ApplicationContext
单例bean默认是立即加载的。 - 扩展性:
BeanFactory
不包含对AOP的支持,通常用于Java SE环境和轻量级Java EE应用。ApplicationContext
提供了对AOP的支持。 - 事件机制:
BeanFactory
不提供事件传播和消息发布机制。ApplicationContext
支持事件传播和消息发布,可以使用ApplicationEvent
和相关监听器。 - 国际化功能:
BeanFactory
不支持国际化功能。ApplicationContext
支持国际化功能。 - 应用服务器:
BeanFactory
不适用于Web应用。ApplicationContext
适合Web应用,通常与Spring的MVC框架结合使用。 - Web应用:
ApplicationContext
提供了对Web应用的支持,如WebApplicationContext
。
总结
BeanFactory
是最基本的IoC容器,提供了简单的依赖注入功能。可以理解为含有Bean的集合工厂类,便于在接收到客户端请求时将对应的bean实例化。BeanFactory
还包含了对bean生命周期的控制,以及调用客户端的初始化方法和销毁方法。ApplicationContext
不仅包含了BeanFactory
的所有功能,还扩展了其他如事件处理、国际化、AOP等高级特性,更适合完整的企业级应用。
在实际应用中,大多数开发者会使用ApplicationContext
,因为它提供了更多的便利功能。然而,如果应用不需要这些额外的功能,使用BeanFactory
可能会减少一些容器的开销。
六、Bean的不同配置方式
在Spring框架中,存在多种配置Bean的方式,每种方式都有其特定的使用场景和特点。以下是一些常见的Spring Bean配置方式:
-
XML配置:
- 通过传统的XML文件定义beans。这种方式在Spring早期版本中非常常见。
<bean id="myBean" class="com.example.MyBean"> <property name="property" value="value"/> </bean>
-
注解配置:
- 从Spring 2.5开始,可以使用注解在类上直接声明Bean,免去了XML配置的繁琐。
@Component
、@Service
、@Repository
、@Controller
等注解可以标识一个类作为Spring管理的Bean。
@Component public class MyComponent { // ... }
-
Java配置:
- Spring 3引入了基于Java的配置方式,可以使用
@Configuration
注解的类来提供配置信息。 - 使用
@Bean
注解方法来声明Spring容器管理的Bean。
@Configuration public class AppConfig { @Bean public MyBean myBean() { return new MyBean(); } }
- Spring 3引入了基于Java的配置方式,可以使用
-
组件扫描:
- 通过
@ComponentScan
注解指定包路径,Spring会自动扫描该包及其子包中的带有@Component
、@Service
、@Repository
、@Controller
等注解的类,并将它们注册为Bean。
@Configuration @ComponentScan(basePackages = "com.example.package") public class AppConfig { // ... }
- 通过
-
自动装配:
- 使用
@Autowired
注解,Spring容器能自动注入依赖的Bean,减少了配置的复杂性。
public class MyComponent { @Autowired private MyDependency myDependency; }
- 使用
-
环境特定配置:
- 使用
@Profile
注解可以指定某些Bean只在特定的环境下创建,例如开发环境或测试环境。
@Configuration @Profile("development") public class DevConfig { @Bean public DataSource dataSource() { // 配置开发环境的数据库连接 } }
- 使用
-
基于Groovy的脚本配置:
- Spring允许使用Groovy语言编写的脚本进行Bean定义,这为那些喜欢动态语言的开发者提供了便利。
-
使用Spring Boot的自动配置:
- Spring Boot提供了大量的自动配置类,这些类可以根据添加的jar依赖和其他因素自动配置Bean。
-
使用
@Bean
注解的普通方法:- 在配置类中,可以通过带有
@Bean
注解的方法定义Bean,Spring容器会调用这些方法并注册返回值。
- 在配置类中,可以通过带有
每种配置方式都有其优缺点,开发者可以根据项目的具体需求和个人喜好选择最合适的配置方式。随着Spring Boot的流行,基于注解和Java配置的方式越来越受到开发者的青睐。
七、Spring Bean的生命周期流程
-
实例化BeanDefinition:
容器首先读取配置元数据(XML、注解或Java配置),并创建BeanDefinition
对象。 -
BeanFactoryPostProcessor:
BeanFactoryPostProcessor
的postProcessBeanFactory
方法被调用,允许修改BeanFactory的基本设置和预处理BeanDefinition
。 -
BeanPostProcessor:
BeanPostProcessor
的postProcessBeforeInstantiation
方法被调用,这是在Bean实例化之前调用的。 -
实例化Bean:
如果postProcessBeforeInstantiation
返回null,则容器将根据BeanDefinition
实例化Bean。 -
InstantiationAwareBeanPostProcessor:
如果实现了InstantiationAwareBeanPostProcessor
接口,其postProcessBeforeInstantiation
方法会被调用。 -
属性填充:
容器将属性注入到Bean中。 -
BeanNameAware和BeanFactoryAware:
如果Bean实现了BeanNameAware
或BeanFactoryAware
接口,相应的setBeanName
或setBeanFactory
方法将被调用。 -
初始化前BeanPostProcessor:
BeanPostProcessor
的postProcessBeforeInitialization
方法被调用。 -
自定义初始化方法:
如果Bean定义了init-method
,该方法将被调用。 -
初始化后BeanPostProcessor:
BeanPostProcessor
的postProcessAfterInitialization
方法被调用。 -
Destruction:
当容器关闭时,将触发以下过程:DisposableBean
的destroy
方法被调用。- 如果Bean定义了
destroy-method
,该方法将被调用。
八、谈谈你对spring框架的理解
Spring是一个一站式的轻量级框架。它为开发Java应用程序提供了全面的基础架构支持。 Spring思想是将应用程序的主动性改为被动性,使得开发者可以专注于应用程序的业务逻辑。Spring框架主要有以下核心特性和概念:
-
控制反转(IOC)容器:
- Spring框架的核心是IOC容器,它负责管理对象的创建、配置和组装。IOC容器通过配置元数据(XML、注解或Java配置类)来管理对象的生命周期。
-
面向切面编程(AOP):
- Spring提供了AOP支持,允许开发者将横切关注点(如日志记录、事务管理等)与业务逻辑分离,从而提高代码的模块化和可维护性。
-
事务管理:
- Spring提供了声明式和编程式的事务管理,支持多种事务管理器,如JDBC、Hibernate、JPA等。
-
数据访问:
- Spring提供了对各种数据访问技术的集成,包括JDBC、ORM框架(如Hibernate、JPA)和NoSQL数据库。
-
Web应用开发:
- Spring支持构建Web应用程序,包括Spring MVC框架和Spring WebFlux(响应式编程模型)。
-
安全性:
- Spring Security是一个功能强大的安全模块,提供了认证、授权和保护Web应用程序的支持。
-
Spring Boot:
- Spring Boot是Spring的一个模块,它简化了Spring应用的创建、部署和运维,提供了自动配置、嵌入式服务器和无XML配置等特性。
-
Spring Cloud:
- Spring Cloud是一系列框架的集合,用于简化分布式系统的开发,包括服务发现、配置管理、断路器、API网关等。
-
模块化:
- Spring框架是高度模块化的,由20多个模块组成,开发者可以按需选择使用。
-
集成测试:
- Spring提供了对各种测试策略的支持,包括单元测试(如JUnit)、集成测试和Web测试。
-
依赖注入(DI):
- Spring通过DI促进了松耦合,使得组件之间的依赖关系可以通过容器进行注入,而不是硬编码。
-
事件驱动:
- Spring提供了事件发布和监听机制,允许在Spring上下文中发布和消费事件。
-
国际化:
- Spring支持国际化,可以处理多语言环境。
-
RESTful Web服务:
- Spring MVC支持创建RESTful风格的Web服务。
总的来说,Spring是一个强大且灵活的Java企业应用程序开发框架,它通过提供一系列非侵入式服务,简化了开发复杂企业应用程序的过程。
九、Spring的三级缓存知道吗?
Spring中的三级缓存指的是Bean的创建过程中涉及到的缓存机制,三级缓存分别是singletonObjects
、earlySingletonObjects
和singletonFactories
。
singletonObjects
缓存存储已经完全初始化的Bean实例。earlySingletonObjects
缓存存储尚未完全初始化的Bean实例,通常用于解决循环依赖或提前暴露的情况。singletonFactories
缓存存储Bean的创建工厂对象,用于解决循环依赖的问题。
这三级缓存机制协同工作,确保了Bean的正常创建和管理。
十、Spring的三级缓存解决循环依赖
所谓循环依赖,假设有两个类A和B,类A依赖类B,类B中依赖类A。这就形成了循环依赖。
当Spring容器初始化时:
- Spring开始创建BeanA,实例化后放入二级缓存
earlySingletonObjects
。在创建BeanA的过程中,它需要依赖BeanB,因此Spring开始创建BeanB。 - Spring将刚实例化但尚未注入依赖的BeanB放入二级缓存
earlySingletonObjects
,并开始注入BeanB的属性。 - 在为BeanB注入依赖时,它需要BeanA的引用。此时,BeanA尚未完全初始化,但Spring可以从一级缓存
singletonObjects
中获取BeanA的引用(因为BeanB的创建过程中,BeanA已经完全初始化)。 - 由于BeanA需要注入一个尚未完全初始化的BeanB,Spring从三级缓存
singletonFactories
中获取BeanB的工厂对象(ObjectFactory),并通过该工厂对象获取BeanB的早期引用(尚未注入依赖的BeanB)。 - Spring将这个早期引用注入到BeanA中,然后完成BeanA的初始化,并将其放入一级缓存
singletonObjects
。 - 随后,Spring继续完成BeanB的初始化,并将完全初始化好的BeanB放入一级缓存
singletonObjects
。
通过上述过程,即使BeanA和BeanB存在循环依赖,Spring也能够通过三级缓存机制解决它们之间的依赖关系,确保每个Bean都能获得对方的引用,而不会导致死循环。
十一、在Spring中,除了三级缓存机制,还有哪些其他方式可以解决循环依赖问题?
在Spring框架中,除了使用三级缓存机制来解决单例Bean的循环依赖问题,还可以采用以下几种方法:
-
设计优化:
- 重构代码:重新设计类和服务,消除循环依赖。这是最推荐的方法,因为它可以使代码更清晰、更易于维护。
-
使用
@PostConstruct
注解:- 通过在Bean的生命周期中稍后初始化某些属性,可以使用
@PostConstruct
注解来避免构造函数中的循环依赖。
- 通过在Bean的生命周期中稍后初始化某些属性,可以使用
-
使用
@Lookup
注解:- 在Java EE 5中引入的
@Lookup
注解可以用来在不违反Bean封装的情况下,获取另一个Bean的引用。然而,这种方法并不推荐,因为它破坏了控制反转的原则。
- 在Java EE 5中引入的
-
使用
@Profile
注解:- 有时候,可以通过为不同的环境配置使用不同的Bean来避免循环依赖。
-
使用不同的Bean Scope:
- 改变Bean的作用域,例如,使用
@Scope("prototype")
注解将Bean的作用域从单例改为原型,可以避免一些循环依赖问题。
- 改变Bean的作用域,例如,使用
-
使用
ObjectFactory
或ObjectProvider
:- 通过使用
ObjectFactory
或ObjectProvider
来延迟Bean的注入,可以避免在某些情况下的循环依赖。
- 通过使用
-
使用
@Order
或@Priority
注解:- 通过确保Bean的创建顺序,可以在一定程度上缓解循环依赖问题。
-
使用
@Autowired
的required
属性:- 将
@Autowired
注解的required
属性设置为false
,可以避免在找不到注入的Bean时抛出异常,但这并不解决循环依赖,只是推迟了问题。
- 将
-
使用中间Bean:
- 有时候,可以通过引入一个中间Bean来解决两个Bean之间的循环依赖问题。
-
使用
@Qualifier
注解:- 当有多个同一类型的Bean可供注入时,可以使用
@Qualifier
注解来指定注入哪一个,这有助于解决某些情况下的循环依赖。
- 当有多个同一类型的Bean可供注入时,可以使用
十二、Spring 的常用注解
Spring框架中使用了大量的注解来简化企业级应用的开发。以下是一些Spring中常用的注解:
核心注解
@Autowired
:自动注入依赖的Bean。@Component
:表示一个受Spring管理的组件。@Service
:表示一个服务层(Service Layer)的组件。@Repository
:表示一个数据访问层(Repository Layer)的组件。@Controller
:表示一个表现层(Presentation Layer)的组件,如一个Web控制器。@Configuration
:表示一个Java配置类,可以替代传统的XML配置文件。
组件扫描
@ComponentScan
:定义组件扫描的路径。@Profile
:用于指定组件在哪个环境的配置下才能被注册到容器中。
配置注解
@Bean
:用于在Java配置类中声明一个Bean。@Value
:将配置文件中的值注入到Bean的字段中。@PropertySource
:用于指定配置文件的位置。
行为注解
@Transactional
:声明事务管理。@Lazy
:用于指定Bean的懒加载。
继承结构注解
@Inherit
:允许子类继承父类的配置。
其他注解
@PostConstruct
:在Bean的初始化之后调用的方法。@PreDestroy
:在Bean销毁之前调用的方法。@Qualifier
:当有多个同一类型的Bean时,用于指定具体注入哪一个。@Primary
:用于指定首选的Bean。@DependsOn
:用于指定Bean的初始化顺序。
数据访问注解
@Entity
:标识一个实体注解,配合JPA使用。@Repository
:标识持久层组件(即DAO组件)。@OneToOne
、@OneToMany
、@ManyToOne
、@ManyToMany
:JPA中的关联注解。
切面注解
@Aspect
:声明一个切面。@Before
、@After
、@Around
:用于切面的前置、后置和环绕通知。@Pointcut
:用于定义一个切入点。
十三、Spring MVC的常用注解
@RequestMapping
:用于将HTTP请求映射到Controller的处理方法上。@RequestParam
:用于请求参数的绑定。@PathVariable
:用于将URI模板变量绑定到控制器处理方法的参数上。@ModelAttribute
:用于请求的输入数据和业务模型对象的绑定。@RestController
:用于标注一个类,是Controller的特化,它表明该类中的所有方法直接返回模型数据。
Web相关注解
@SessionAttributes
:用于会话级属性的支持。@CookieValue
:用于从HTTP请求中获取Cookie值。
十四、Spring Security的常用注解
@Secured
:用于标识安全权限。@PreAuthorize
、@PostAuthorize
、@PreFilter
、@PostFilter
:Spring表达式语言(SpEL)的安全注解。
十五、SpringBoot的常用注解有哪些?
Spring Boot 旨在简化新Spring应用的创建和开发过程。它通过提供一系列便捷的注解,让开发者能够轻松地配置和启动应用。以下是Spring Boot中的一些常用注解:
核心注解
@SpringBootApplication
:组合注解,用于启动Spring Boot应用,包含@Configuration
、@EnableAutoConfiguration
和@ComponentScan
。
配置相关注解
@EnableAutoConfiguration
:告诉Spring Boot根据添加的jar依赖自动配置项目。@EnableComponentScan
:启用Spring组件扫描,通常用在非主类上。@EnableXXXX
(如@EnableWebMvc
):针对特定功能的注解,用以开启Spring Boot的自动配置。
Web 相关注解
@RestController
:组合注解,用于定义RESTful Web服务,等同于@Controller
和@ResponseBody
。@RequestMapping
:用于将HTTP请求映射到Controller的处理方法上。@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
:分别用于处理HTTP GET、POST、PUT、DELETE请求的快捷方式。@PathVariable
:用于将URI模板变量绑定到控制器处理方法的参数上。@RequestParam
:用于将请求参数绑定到控制器处理方法的参数上。@RequestBody
:用于读取Http请求正文,将其转换为Java对象。@ResponseBody
:表示该方法的返回结果直接写入HTTP响应正文中,用于异步请求处理。
数据访问相关注解
@Entity
:用于标识实体注解,与JPA一起使用。@Repository
:用于标识持久层组件(即DAO组件),可以用于注解DAO接口或类。@Service
:用于标识服务层(业务层)的组件。@Controller
:用于标识控制层的组件,如 MVC 中的控制器。@RestController
:用于标识控制层的组件,同时标记该控制器中的方法返回对象直接作为响应正文,而不是视图,因此它组合了@Controller
和@ResponseBody
。
其他常用注解
@ConfigurationProperties
:用于将配置文件中的值绑定到一个对象上。@Value
:用于将配置文件中的值注入到Bean的字段中。@PropertySource
:用于指定配置文件的位置。@Bean
:用于在Java配置类中声明一个Bean。@Profile
:用于指定组件在哪个环境的配置下才能被注册到容器中。@Import
:用于导入其他配置类或者导入组件注册到容器中。@ComponentScan
:用于定义组件扫描的路径。
十六、Spring Boot Actuator 相关注解
@Endpoint
:用于定义一个健康检查或信息报告端点。@Readonly
:用于指示一个端点是只读的,即它不会改变应用的状态。
十七、Spring和SpringBoot的自动装配了解过吗?
Spring的自动装配
可以大幅度减少Spring配置,方便编程,但也会造成依赖不能明确管理,可能会有多个bean同时符合注入规则,没有清晰的依赖关系。自动装配有两种方式:byName和byType。
byName:根据属性名自动装配。检查容器并根据名字查找与属性完全一致的bean。
byType:根据容器中属性类型相同的bean自动装配。如果存在多个该类型bean,那么抛出异常。
SpringBoot的自动装配
指的是Springboot会自动将一些配置类的bean注册到IOC容器中,只需要引入功能包,其他的配置就完全交由Springboot自动注入,然后我们可以在需要的地方使用@Autowired或@Resource等注解来使用它。
十八、谈谈你对SpringBoot的理解
Spring Boot是一个全新的搭建JavaEE应用程序的高级框架,它设计目的是简化新Spring应用的创建和开发过程。Spring Boot的核心特性包括:
-
独立运行:
Spring Boot应用可以独立运行,不需要部署到外部的Web服务器上,因为Spring Boot内置了Tomcat、Jetty等Servlet容器。 -
简化配置:
通过自动配置和约定优于配置的原则,Spring Boot简化了大部分Spring应用的配置。 -
无需XML配置:
Spring Boot不需要使用XML配置,可以通过Java配置类来设置你的Spring应用。 -
Spring生态系统:
它继承了Spring Framework的优势,可以与Spring Data、Spring Security、Spring Cloud等其他Spring模块无缝集成。 -
微服务支持:
Spring Boot非常适合微服务架构的应用开发,它提供了一系列组件来支持微服务的相关特性,如服务发现、断路器、分布式配置等。 -
执行器和监控:
Spring Boot Actuator提供了一系列的生产级别的特性,包括应用的监控和管理。 -
社区支持:
Spring Boot有一个活跃的社区,你可以获得大量的工具和库来支持你的开发。 -
简化部署:
通过其内嵌的Servlet容器,Spring Boot应用可以打包成一个单一的、可执行的JAR或WAR文件,简化了部署过程。 -
安全特性:
Spring Boot提供了自动配置的Spring Security支持,可以快速添加安全特性。 -
其他特性:
包括健康检查、度量信息、外部配置支持等。
Spring Boot的目标是尽可能地简化和加速Spring应用的整个开发周期,从开始的创建项目到部署运行。它通过提供一系列非功能性的通用配置,让开发者专注于应用的核心业务逻辑。
使用Spring Boot的好处是显而易见的:它减少了项目配置量,提高了开发效率,并且使得应用的部署和运维变得更加容易。同时,Spring Boot的“opiniated”(有主见的)配置方式,为常见的使用场景提供了合适的默认设置,降低了新手的入门门槛。
十九、搭建Springboot框架时,框架的异常是怎么配的?
系统框架搭建时,为了约束代码规范,我们会对一些通用功能做一些处理,比如声明一些系统公用错误类、封装通用返回结果、统一异常处理等。Spring Boot提供了几种不同的方法来配置和处理异常:
- 启用Spring Boot的默认异常处理
Spring Boot默认提供了一个错误处理机制,它基于Spring MVC的@ControllerAdvice和@ExceptionHandler注解。你可以在框架中声明一个全局异常处理类,并添加@ControllerAdvice和@RestController,使用@ExceptionHandler注解在类中配置异常方法处理,包括空指针异常、IO异常、权限不足异常等。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<String> handleAllExceptions(Exception ex, WebRequest request) {
// 可以获取异常信息和请求信息
return new ResponseEntity<>("Global Exception: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
- 声明系统通用错误处理类,对常用的异常错误设置返回code
声明一个系统通用错误处理类来对常用的异常错误设置返回码是异常处理的一个关键部分。在Spring Boot应用中,这通常是通过创建一个带有@ControllerAdvice注解的类来完成的,该类可以捕获并处理全局范围内的异常。比方说,对于“参数错误”返回100000,对于“系统错误”,返回100001。
@ControllerAdvice
public class GlobalExceptionHandler {
// 通用异常处理
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse("系统错误", "10001");
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
// 特定异常处理
@ExceptionHandler(CustomNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(CustomNotFoundException ex) {
ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), "20001");
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
// 错误响应对象
@Data
public static class ErrorResponse {
private String message;
private String code;
public ErrorResponse(String message, String code) {
this.message = message;
this.code = code;
}
}
}
- 声明全局异常处理类中的结果返回类。
专门添加一个类用来存放处理结果信息的方法,如输出错误信息500、输出带数据的成功信息200、输出结果result。
@ControllerAdvice
public class GlobalExceptionHandler {
// 统一的异常处理方法
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
// 根据异常类型确定HTTP状态码,这里以500服务器错误为例
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
// 可以添加逻辑来根据不同类型的异常设置不同的HTTP状态码
// ...
// 创建错误响应对象,包含错误信息和状态码
ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), status);
// 返回包含错误信息的ResponseEntity对象
return new ResponseEntity<>(errorResponse, status);
}
// 定义统一的错误响应类
@Data
public static class ErrorResponse {
private String timestamp;
private String message;
private Integer status;
private String error;
public ErrorResponse(String message, HttpStatus status) {
this.timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
this.message = message;
this.status = status.value();
this.error = status.getReasonPhrase();
}
}
}
二十、Property yaml 文件里面配的参数可以通过哪些方式获取?
- 使用@Value注解读取方式
public class MyComponent {
@Value("${my.property}")
private String myProperty;
}
- 使用Environment.getProperty()读取
public class MyComponent {
private final Environment env;
@Autowired
public MyComponent(Environment env) {
this.env = env;
}
public String getProperty() {
return env.getProperty("my.property");
}
}
- 使用@PropertySource + @ConfigurationProperties注解
@Configuration
@PropertySource(value = "classpath:another.properties", factory = YamlPropertySourceFactory.class)
public class MyConfig {
// ...
}
- 使用@Profile注解
结合使用@Profile注解和配置文件以根据不同环境加载不同的配置。
# application-local.yml
my:
property: devValue
@Configuration
@Profile("local")
public class DevConfig {
@Value("${my.property}")
private String myProperty;
// ...
}
- 使用命令行参数
java -jar app.jar --my.property=overrideValue
- 使用Java System Properties
java -Dmy.property=overrideValue -jar app.jar
二一、SpringMVC的运作流程?
Spring MVC是Spring框架的一个模块,用于构建Web应用程序。它基于MVC(Model-View-Controller)设计模式,将应用程序分为模型、视图和控制器三个部分,以实现关注点分离。
1、Spring MVC的运作流程
-
客户端请求:
用户通过浏览器发送HTTP请求到服务器。 -
前端控制器(DispatcherServlet):
请求首先到达前端控制器DispatcherServlet
,它是Spring MVC的入口点。 -
请求映射处理器(HandlerMapping):
DispatcherServlet
将请求委托给请求映射处理器,该处理器负责将请求映射到相应的处理器(Controller
)。 -
处理器适配器(HandlerAdapter):
找到合适的处理器后,请求被委托给处理器适配器。适配器负责调用处理器,并将其返回值(模型)转换成适当的响应。 -
处理器(Controller):
处理器执行业务逻辑,处理请求,并返回一个模型。 -
视图解析器(ViewResolver):
处理器返回的模型数据需要被渲染成视图。视图解析器负责解析逻辑视图名,并根据解析的视图名找到相应的视图模板。 -
视图(View):
视图负责将模型数据渲染成HTML页面,并发送给客户端。 -
模型和视图:
在Spring MVC中,模型是业务逻辑处理的结果,视图是展示模型数据的界面。 -
响应:
视图渲染完成后,将响应发送回客户端浏览器。
2、Spring MVC的关键组件
- DispatcherServlet:前端控制器,处理用户的请求,并将其转发到相应的处理器。
- HandlerMapping:处理器映射器,负责根据请求找到合适的处理器。
- Controller:处理器,负责处理用户的业务逻辑。
- HandlerAdapter:处理器适配器,任何自定义的Controller必须有一个适配器与DispatcherServlet进行交互。
- ViewResolver:视图解析器,根据逻辑视图名解析成真正的视图。
- View:视图,渲染模型数据为用户可见的网页。
3、Spring MVC的特点
- 清晰的角色划分:控制器、模型、视图都扮演着不同的角色。
- 支持REST:可以轻松实现RESTful风格的请求处理。
- 支持多种视图技术:如Thymeleaf、FreeMarker、JSP等。
- 支持多种数据格式:如JSON、XML等。
- 灵活性和扩展性:可以通过自定义组件来扩展框架的功能。
Spring MVC通过以上流程和组件,为开发者提供了一个灵活、可扩展的Web应用程序开发框架。
微服务架构篇
一、集群、分布式、微服务概念和区别?
1、集群(Cluster)
定义:集群是一组紧密连接的计算机(节点),它们协同工作,对外表现为一个单一的系统。集群中的每个节点运行相同的软件,并且共享存储和其他资源。
特点:
- 高可用性:如果一个节点失败,另一个节点可以接管其任务,以避免系统中断。
- 负载均衡:集群可以在多个节点之间分配工作负载,提高处理能力和响应时间。
- 透明性:客户端通常不知道后端的复杂性,它们只与一个逻辑系统交互。
2、分布式(Distributed)
定义:分布式系统是由多个独立的计算机(节点)组成的,这些计算机通过网络连接,协同工作,但每个节点都有自己的本地内存和处理器。
特点:
- 组件自治:每个节点在程序执行和本地资源管理方面都是自治的。
- 协同工作:节点通过网络通信和数据交换来协作完成任务。
- 缺乏全局状态:分布式系统中没有单一的全局状态,每个节点可能只了解部分系统状态。
3、微服务(Microservices)
定义:微服务架构是一种开发方法,将一个应用程序构建为一系列小型服务的集合,每个服务实现特定的业务功能,并可以独立部署和升级。
特点:
- 独立部署:每个微服务都是独立的,可以单独部署、升级和扩展。
- 业务导向:每个服务围绕特定的业务能力构建,拥有自己的业务逻辑。
- 技术多样性:不同的微服务可以使用不同的编程语言、数据库或其他存储技术。
4、区别:
- 集群侧重于通过将多个计算机作为一个单元来提高系统的可用性和性能。
- 分布式侧重于通过网络连接多个独立的计算机来实现更广泛的系统功能和扩展性。
- 微服务侧重于将应用分解为可以独立开发、测试、部署和运行的小服务。
在实践中,这些概念经常交织在一起。例如,一个微服务架构的系统可能是分布式的,并且为了提高可用性和可伸缩性,它的每个服务都运行在集群中。此外,分布式系统可以包含多个微服务,而微服务架构通常部署在分布式环境中。选择这些架构风格取决于具体的业务需求、技术挑战和组织能力。
二、传统的web服务于Restful风格的服务的区别?
传统的Web服务和RESTful风格的服务都是网络应用程序之间进行通信的方法,但它们在设计理念、通信协议、数据格式和操作方式上存在一些关键区别:
1、传统的Web服务(SOAP Web Services)
-
协议:通常使用SOAP(Simple Object Access Protocol)作为通信协议。SOAP是XML(可扩展标记语言)的一个子集,用于在系统间交换结构化信息。
-
数据格式:主要使用XML作为数据交换格式,也可以使用JSON。
-
服务描述:使用WSDL(Web Services Description Language)来描述服务,包括服务的接口、消息格式和协议绑定。
-
操作风格:通常基于操作(Operation-centric),每个SOAP消息对应一个远程过程调用(RPC)。
-
协议绑定:可以支持多种协议绑定,如HTTP、SMTP、FTP等。
-
服务发现:使用UDDI(Universal Description, Discovery, and Integration)来注册和发现Web服务。
-
事务性:支持ACID(Atomicity, Consistency, Isolation, Durability)事务。
-
安全性:通常使用WS-Security等安全协议。
2、RESTful风格的服务(RESTful Web Services)
-
协议:主要使用HTTP协议,它是Web应用最常用的协议。
-
数据格式:可以使用XML,但更倾向于使用JSON,因为JSON更加轻量和易于阅读。
-
服务描述:不使用WSDL,而是通过HTTP的URI(统一资源标识符)来标识资源。
-
操作风格:基于资源(Resource-centric),使用HTTP方法(GET、POST、PUT、DELETE等)来操作资源。
-
协议绑定:主要绑定到HTTP协议。
-
服务发现:通常不使用UDDI,而是通过URI和文档(如API文档)来发现服务。
-
事务性:不强调ACID事务,而是倾向于最终一致性。
-
安全性:可以使用HTTP基本认证、OAuth、JWT(JSON Web Tokens)等安全机制。
3、主要区别
- 通信风格:SOAP是面向操作的,而REST是面向资源的。
- 数据格式:SOAP通常使用XML,REST更倾向于使用JSON。
- 服务描述:SOAP使用WSDL,REST不使用WSDL。
- 协议:SOAP可以支持多种协议,而REST主要使用HTTP。
- 服务发现:SOAP使用UDDI,REST通常不使用UDDI。
- 事务性:SOAP支持ACID事务,REST倾向于最终一致性。
- 安全性:SOAP使用WS-Security,REST使用HTTP安全机制。
RESTful服务因其简单性、灵活性和轻量级特性,在现代Web API设计中越来越受欢迎。
三、RPC服务和Restful服务的区别?
RPC(Remote Procedure Call,远程过程调用)服务和RESTful(Representational State Transfer,表现层状态转移)服务都是实现客户端和服务器之间通信的架构风格,但它们在设计理念、通信方式、数据交换格式等方面存在一些关键的区别:
1、RPC服务
-
编程模型:RPC允许开发者像调用本地函数一样调用远程服务,隐藏了网络通信的细节。
-
协议:RPC可以基于TCP/IP或其他协议,没有固定的标准协议。
-
数据交换格式:RPC可以使用二进制格式(如Protocol Buffers)、XML或JSON,具体取决于实现。
-
服务定义:RPC通常使用接口定义语言(IDL)来定义服务,如gRPC使用Protocol Buffers。
-
服务发现:RPC可能需要服务注册和发现机制,如使用ZooKeeper或etcd。
-
传输:RPC可能使用同步或异步传输,具体取决于实现。
-
安全性:RPC框架可能提供自己的安全机制,如认证和加密。
-
跨语言:RPC框架可能支持多种编程语言,但需要每个语言特定的客户端库。
2、RESTful服务
-
架构风格:RESTful服务基于HTTP协议,使用无状态的、标准的Web技术。
-
资源导向:RESTful服务通过URI(统一资源标识符)操作资源,使用HTTP方法(GET、POST、PUT、DELETE)来执行CRUD操作。
-
数据交换格式:RESTful服务通常使用JSON和XML作为数据交换格式。
-
状态无限制:RESTful服务应该是无状态的,每个请求必须包含所有必要的信息来处理请求。
-
服务发现:RESTful服务通常不依赖于服务发现机制,直接通过URI访问。
-
传输:RESTful服务通常使用HTTP的GET和POST方法进行同步传输。
-
安全性:RESTful服务使用HTTP安全机制,如HTTPS、OAuth、JWT等。
-
跨语言和跨平台:RESTful服务不依赖于特定的编程语言或平台,任何可以发送HTTP请求的客户端都可以访问。
3、主要区别:
- 编程模型:RPC隐藏了网络通信的细节,更接近本地函数调用;RESTful服务强调资源和HTTP方法。
- 协议:RPC不局限于HTTP,可以使用其他协议;RESTful服务基于HTTP。
- 数据交换格式:RPC可以使用二进制格式,更高效;RESTful服务通常使用JSON和XML。
- 服务发现:RPC可能需要服务发现机制;RESTful服务直接通过URI访问。
- 跨语言和跨平台:RPC需要每个语言特定的客户端库;RESTful服务不依赖于特定的编程语言或平台。
总结
- 从本质区别上看,RPC是基于TCP实现的,RESTful是基于HTTP来实现的。
- 从传输速度上来看,因为HTTP封装的数据量更多所以数据传输量更大,所以RPC的传输速度是比RESTFUL更快的。
- 因为HTTP协议是各个框架都普遍支持的。在toC情况下,因为不知道情况来源的框架、数据形势是什么样的,所以在网关可以使用Restful利用http来接受。而在微服务内部的各模块之间因为各协议方案是公司内部自己定的,所以知道各种数据方式,可以使用TCP传输以使各模块之间的数据传输更快。所以可以网关和外界的数据传输使用RESTFUL,微服务内部的各模块之间使用RPC。
- RESTful的API的设计上是面向资源的,对于同一资源的获取、传输、修改可以使用GET、POST、PUT、DELETE来对同一个URL进行区别,而RPC通常把动词直接体现在URL上
四、Restful的六大原则?
RESTful架构风格是由Roy Fielding在其博士论文中提出的,RESTful API设计遵循以下六大原则:
-
无状态(Stateless):
每个请求从客户端到服务器必须包含所有必要的信息来理解和处理请求。服务器不会存储任何客户端请求之间的状态信息,这有助于提高性能和可伸缩性。 -
统一接口(Uniform Interface):
RESTful API必须拥有一个统一的接口,这通常意味着所有的操作都是通过标准的HTTP方法(如GET、POST、PUT、DELETE)来完成的。 -
资源导向(Resource-Oriented):
在RESTful架构中,所有事物都被视为资源,这些资源通过URI(统一资源标识符)来标识。资源的表述(如JSON、XML)可以通过HTTP消息的主体传输。 -
通过超媒体作为应用状态的引擎(HATEOAS):
客户端-服务器交互时,客户端应该能够发现所有可用的资源和资源之间的关系,这通常是通过超媒体链接实现的,即返回的资源表述中包含链接到其他资源的URL。 -
客户端-服务器分离(Client-Server Separation):
客户端负责用户界面和用户体验,而服务器端负责业务逻辑和数据存储。它们之间的交互应该通过RESTful API来实现。 -
可缓存(Cacheable):
对于客户端的GET请求,服务器应该提供明确的缓存指示,以便客户端可以缓存响应结果。这有助于提高效率和性能。
遵循这些原则可以帮助设计出易于使用、可伸缩、可维护的RESTful API。然而,实际应用中,并非所有API都必须严格遵循所有原则,特别是在HATEOAS原则方面,许多现有的RESTful API并未完全实现。设计者可以根据业务需求和实际情况灵活应用这些原则。
五、Controller层输入规范
在构建RESTful API时,Controller(控制器)是处理客户端请求和生成响应的核心组件。Controller的输入规范通常遵循以下最佳实践:
在构建RESTful API时,Controller(控制器)是处理HTTP请求和返回响应的核心组件。Controller的输入规范通常遵循REST原则和HTTP协议的标准。以下是一些通用的Controller输入规范:
- 使用HTTP方法表达意图:
- GET:读取资源。
@GetMapping("/getResource/{id}")
public ResponseEntity<?> getResourceById(@RequestParam("id") Long id) {
// 获取资源逻辑
}
- POST:创建新资源。
@PostMapping("/createResource")
public ResponseEntity<?> createResource(@RequestBody Resource resource) {
// 创建资源逻辑
}
- PUT:更新现有资源。
@PutMapping("/updateResource/{id}")
public ResponseEntity<?> updateResource(@PathVariable Long id) {
// 更新资源逻辑
}
- DELETE:删除资源。
@DeleteMapping("/deleteResource/{id}")
public ResponseEntity<?> deleteResource(@PathVariable Long id) {
// 删除资源逻辑
}
-
使用URI(统一资源标识符)标识资源:
URI应该清晰、直观,并与资源直接关联。例如,/users/123
表示用户资源的特定实例。 -
使用请求体发送数据:
- 在创建或更新资源时,使用请求体(payload)发送数据。例如,使用JSON格式在POST或PUT请求中发送用户数据。
-
使用查询参数进行过滤、排序和分页:
- 通过URL的查询字符串添加过滤、排序和分页参数。例如,
/users?name=John&sort=asc&page=2
。
- 通过URL的查询字符串添加过滤、排序和分页参数。例如,
-
使用标准HTTP状态码:
- 使用合适的HTTP状态码来表示操作的结果,如200 OK、201 Created、404 Not Found、400 Bad Request等。
-
使用JSON或XML格式传输数据:
- RESTful API通常使用JSON作为传输数据的格式,因为它轻量且易于阅读。XML也是可选的,但使用较少。
-
使用分页和限制响应大小:
- 对于可能返回大量数据的请求,使用分页来限制响应的大小,提高性能。
-
输入验证:
- 对所有输入进行验证,确保数据的合法性和安全性。
-
错误处理:
- 在出现错误时返回合适的错误信息和状态码,例如使用422 Unprocessable Entity状态码表示验证错误。
-
安全性:
- 确保遵守安全最佳实践,如使用HTTPS、输入验证以防止SQL注入、XSS攻击等。
-
版本控制:
- 在URI或使用特定的HTTP头来管理API版本,以便于维护和向后兼容。
-
遵循HATEOAS原则(如果适用):
- 提供超媒体链接,允许客户端发现所有可用的资源和资源之间的关系。
-
使用适当的内容类型:
- 在请求和响应中使用
Content-Type
头来指示数据格式。
- 在请求和响应中使用
-
避免副作用:
- GET请求和HEAD请求应该只检索数据,不产生副作用。
-
文档和示例:
- 提供清晰的API文档和使用示例,帮助开发者理解和使用API。
遵循这些规范有助于创建一个清晰、一致、易于使用的API,同时提高安全性和性能。不同的框架和语言可能有自己的特定约定和最佳实践,因此在设计Controller时应参考相关的文档。
六、了解过SpringCloud的微服务嘛?了解过哪些组件?
是的,Spring Cloud 是一系列框架的集合,它整合了多种微服务解决方案,并通过Spring Boot风格进行封装,使得开发者可以轻松地构建分布式系统的各种组件。Spring Cloud与Spring Boot紧密集成,以方便开发者在几分钟内构建可运行的微服务。
以下是Spring Cloud的一些主要组件:
-
Eureka:服务中心,用于服务注册与发现。
-
Feign:声明式REST客户端,使得编写Web服务客户端变得更加容易。
-
Hystrix:断路器,用于处理分布式系统的容错,防止服务雪崩。
-
Zuul:网关,用于路由和过滤微服务的请求。
-
Config Server:配置服务器,提供集中化的外部配置管理。
-
Bus:消息总线,用于配置和消息传递。
-
Stream:消息微服务,使用Spring Boot和Project Reactor创建响应式消息微服务。
-
OAuth2:安全认证,提供详细的安全控制和OAuth2协议实现。
-
Sleuth:分布式追踪,与Zipkin结合使用,帮助跟踪请求通过微服务的流动。
-
Turbine:聚合Hystrix的监控数据流。
-
Consul:服务发现与配置,与Eureka类似,但增加了对Consul的支持。
-
Gateway:新一代API网关,作为Zuul的替代品,基于Spring Framework 5的WebFlux。
-
OpenFeign:在Spring Cloud中,Feign已经被OpenFeign取代,它是一个声明式的Web服务客户端,使得编写Web服务客户端变得更加容易。
-
Alibaba Cloud Nacos:Spring Cloud Alibaba中的服务发现和配置管理组件。
-
Alibaba Cloud ACM:应用配置管理,用于集中管理应用配置。
-
Alibaba Cloud OSS:对象存储服务,用于存储和检索数据。
-
Alibaba Cloud SMS:短信服务,用于发送短信通知和验证码。
七、请解释一下SpringCloud和Eureka在项目中的具体应用和优势,以及为什么选择这些技术作为微服务架构的基础
Spring Cloud
是一个用于快速构建分布式系统中的通用模式的工具集。它提供了许多可用于构建微服务架构的功能,如服务发现(Eureka、Consul等)、负载均衡(Ribbon)、断路器(Hystrix)、路由(Zuul)等。使用Spring Cloud
可以帮助我们降低系统的复杂性,提高开发效率,实现了一些重要的微服务框架。Eureka
作为Spring Cloud最为核心的组件之一,是一个分布式的REST心态的服务注册与发现系统,用于提供中央化的服务注册与发现。
在一个典型的微服务架构中,Spring Cloud
和Eureka
的应用具体体现在以下几个方面:
- 服务注册与发现:微服务架构中各个微服务需要动态地发现和调用彼此的服务。
Eureka
作为服务注册中心能够为不同微服务提供自动化的服务注册与发现,让微服务之间的通信变得更加灵活、可靠。这样一来,无论服务部署在任何地方,只要注册到Eureka中,就能在整个网络中找到并调用。 - 弹性和容错:
Spring Cloud
通过集成断路器(例如Hystrix)来帮助微服务系统更好地应对故障和延迟。在微服务之间的通信中,Hystrix能够在一段时间内停止向依赖的服务发出请求,从而避免系统雪崩。结合Ribbon等负载均衡器,可以实现对服务的弹性调用,提高整个微服务架构的稳定性和可靠性。 - 服务路由和网关:
Spring Cloud
中的Zuul组件可以作为服务网关,统一对外提供服务的访问和安全控制。Zuul能够动态地将请求路由到后端的各个微服务,并提供过滤、安全检查、监控等功能。通过Zuul,我们能够更加灵活地管理各个微服务的对外访问。
为什么选择Spring Cloud和Eureka作为微服务架构的基础?
首先,Spring Cloud提供了一整套完备的解决方案,涵盖了微服务架构中的各个方面。其次,Spring Cloud已经在众多企业中得到应用和验证,具有很好的稳定性和可靠性。再者,Spring Cloud与Spring Boot紧密集成,可以帮助开发团队更加轻松地构建和部署微服务应用。最后,Eureka作为服务注册中心,能够提供高效的服务注册与发现机制,使得整个微服务架构能够更加灵活、可扩展和可靠。
八、Spring Cloud 与 Dubbo的区别
Spring Cloud和Dubbo都是目前流行的微服务框架,但它们在设计理念、实现方式和功能特性上存在一些区别:
-
定位差异:
- Spring Cloud:定位为微服务架构下的一站式解决方案,提供包括配置管理、服务发现、断路器、智能路由、微代理、控制总线等在内的一系列分布式系统开发工具。
- Dubbo:起源于SOA时代,主要关注点在于服务的调用和治理,是一个高性能、轻量级的RPC框架。
-
生态差异:
- Spring Cloud:依托于Spring平台,拥有更加完善的生态系统,能够与Spring Framework、Spring Boot、Spring Data等其他Spring项目完美融合。
- Dubbo:起初主要作为RPC远程调用框架,生态相对匮乏,但随着发展也逐渐丰富起来。
-
通信协议:
- Spring Cloud:通常使用HTTP协议进行服务间的调用,接口一般是Rest风格,较为灵活。
- Dubbo:使用自定义的Dubbo协议进行远程调用,基于TCP协议传输,使用Hessian序列化,性能较好。
-
注册中心:
- Spring Cloud:通常使用Eureka作为服务注册和发现中心。
- Dubbo:使用的注册中心可以是Zookeeper或Redis等,具有更灵活的选择。
-
断路器和监控:
- Spring Cloud:使用Hystrix作为断路器,提供服务的熔断和监控功能。
- Dubbo:虽然社区提供了一些支持,但断路器系统的完善程度不如Spring Cloud。
-
网关和路由:
- Spring Cloud:使用Zuul作为微服务网关,提供路由和过滤功能。
- Dubbo:没有内置的服务网关,可能需要额外集成或自研。
-
配置管理:
- Spring Cloud:使用Spring Cloud Config进行集中化的外部配置管理。
- Dubbo:没有专门的分布式配置管理组件,可能需要结合其他工具实现。
-
服务跟踪:
- Spring Cloud:使用Spring Cloud Sleuth进行服务跟踪。
- Dubbo:没有内置的服务跟踪解决方案,可能需要集成第三方服务跟踪系统。
-
社区和维护:
- Spring Cloud:由Spring团队维护,社区活跃,更新频繁。
- Dubbo:曾经一度停止更新,后重启维护,活跃度有所提升,但与Spring Cloud相比可能仍有差距。
-
技术发展:
- Spring Cloud:站在近些年技术发展之上进行开发,因此更具技术代表性。
- Dubbo:虽然技术理念曾经非常先进,但随着技术发展,如果不持续更新,可能会逐渐掉队。
九、在微服务架构中,服务之间的通信方式有哪些,它们的特点和适用场景分别是什么?
在微服务架构中,服务之间的通信方式有多种,其中常见的包括:
1.同步HTTP通信:基于HTTP协议进行同步通信,适用于简单的请求-响应场景,例如前后端交互、服务调用等。特点是通用性强、易于实现,适合于较为简单的场景。
2.异步消息队列:采用消息队列(如RabbitMQ、Kafka等)来进行异步通信,适用于解耦和削峰填谷。特点是消息的异步处理、实现解耦、能够处理大量并发请求,适用于需要提高系统可用性、处理大量消息的场景。
3.RPC(远程过程调用):通过RPC框架(如gRPC、Dubbo等)实现服务之间的远程调用,适用于要求实时性较高、需要精确控制调用流程的场景。特点是能够实现远程调用、支持多语言、性能较高,适用于需要高效通信的场景。
4.事件驱动通信:通过事件驱动机制实现服务之间的通信,例如基于事件的消息总线(Event Bus),适用于实现松耦合、异步通信的场景。特点是实现解耦、扩展性好,适用于需要高度灵活性和可扩展性的场景。
这些通信方式各有特点,根据具体的业务需求和系统架构,可以选择合适的通信方式来实现服务之间的通信。在实际项目中,通常会根据不同的业务场景选择最合适的通信方式,以实现系统的高可用、高性能和易扩展性。
十、服务端微服务地址不小心暴露了,用户就可以绕过网关,直接访问微服务,怎么办?
如果微服务的地址不小心暴露,导致用户可以直接绕过网关访问微服务,这可能会引起安全问题和架构上的问题,如负载均衡、监控、路由等。以下是一些应对措施:
-
立即更改配置:立即更新微服务的配置,避免直接暴露服务地址。
-
使用API网关:确保所有服务都必须通过API网关进行访问。API网关可以提供统一的入口,隐藏内部服务的复杂性。
-
实施访问控制:在微服务层面实施访问控制,如使用Spring Security等框架,确保只有合法的用户和系统才能访问服务。
-
使用防火墙规则:配置防火墙规则,只允许来自网关的流量访问微服务。
-
实施认证和授权:引入OAuth、JWT等认证授权机制,确保即使服务地址暴露,未授权的用户也无法访问敏感数据。
-
使用服务网格:考虑使用Istio、Linkerd等服务网格解决方案,它们可以在微服务间提供安全的通信渠道。
-
限制直接访问的权限:对直接访问微服务的权限进行限制,确保即使暴露,其权限也是有限的。
-
监控和日志:增加对直接访问的监控和日志记录,以便快速发现和处理异常访问。
-
服务发现机制:使用Eureka或其他服务发现机制,确保服务实例不被外部直接访问。
-
使用反向代理:配置反向代理如Nginx,作为额外的安全层,只将特定流量转发到微服务。
-
使用API管理工具:使用API管理工具如Apigee、Amazon API Gateway等,提供额外的安全层和流量管理。
-
实施限流和熔断:使用Hystrix或类似工具对服务进行限流和熔断,防止恶意攻击。
十一、什么是三层架构,四层架构,六边形模型,分层架构,洋葱架构
1、三层架构(3-tier architecture):是一种经典的软件设计模式,它将应用程序分为三个逻辑层次:
- 表示层(Presentation Layer):负责处理用户界面(UI)和用户交互的部分,如网站前端或桌面应用程序的界面。
- 业务逻辑层(Business Logic Layer):包含应用程序的核心业务逻辑,处理数据的加工、计算和业务规则的实施。
- 数据访问层(Data Access Layer):负责与数据库等持久化存储交互,执行数据的增删改查操作。
这种架构有助于分离关注点,提高代码的可维护性和可扩展性。
2、四层架构(4-tier architecture):是三层架构的扩展,将表示层进一步分离为两个层次:
- 用户界面层(User Interface Layer):与三层架构中的表示层相同,负责用户界面和交互。
- 前端业务逻辑层(Front-end Business Logic Layer):处理与用户界面直接相关的业务逻辑,如用户输入的验证和转换。
- 后端业务逻辑层(Back-end Business Logic Layer):与三层架构中的业务逻辑层相同,处理核心业务逻辑。
- 数据访问层:与三层架构中的数据访问层相同,负责数据存储和检索。
四层架构通过分离前端和后端业务逻辑,可以提高应用程序的灵活性和模块化。
3、六边形模型(Hexagonal Architecture):又称为端口与适配器模型(Ports and Adapters Model),是一种更为抽象的架构模式,由Alistair Cockburn提出。它强调应用程序的核心业务逻辑与外部基础设施之间的解耦:
- 业务逻辑(Business Logic):位于六边形的中心,是应用程序的核心,不依赖于任何外部基础设施。
- 端口(Ports):六边形的边缘,代表应用程序的输入(进入端口)和输出(出口端口),定义了与外部世界交互的接口。
- 适配器(Adapters):实现端口定义的接口,将外部请求转换为业务逻辑可以处理的形式,或将业务逻辑的响应转换为外部系统可以理解的形式。
- 外部实体(External Entities):与应用程序交互的外部系统或用户界面,通过适配器与业务逻辑交互。
六边形模型允许系统具有很好的灵活性和可测试性,因为可以通过替换不同的适配器来集成不同的外部系统或模拟外部环境进行单元测试。
4、分层架构(Layered Architecture):通过将系统分解为多个层次来简化大型应用程序的设计。每一层提供特定的功能,并且只与相邻层交互。
- 表示层:用户界面和呈现。
- 业务逻辑层:处理业务规则。
- 数据访问层:与数据库交互。
- 数据层:数据存储。
5、洋葱架构(Onion Architecture):强调层次的顺序和依赖关系,每个层次都是独立的,内层可以无依赖地工作。
- 领域模型:业务逻辑的核心。不依赖于任何外部的基础设施或应用服务。
- 应用层:处理应用程序的用例和业务逻辑,它调用内部的领域逻辑层,并处理来自外部的请求。
- 领域层:实现业务逻辑,可以被应用服务层调用。
- 基础设施层:提供技术实现,如数据库访问、消息队列、外部API调用等。它通过定义的接口(端口)与内部层交互。
十二、网关层中都做了什么?
网关层通常是一个网络系统中的第一个接触点,它可以控制进出系统的数据流并提供一些基本的安全和服务质量保证。具体来说,网关层可能会执行以下功能:
- 路由转发:网关层可以根据网络拓扑、IP地址或其他规则将数据包从一个网络路由到另一个网络。
- 统一访问入口:网关层可以作为整个系统的公共入口,统一处理所有的请求,并将请求分配给相应的下游服务。
- 流量控制:网关层可以限制对下游服务的请求速率,防止系统被过度压力导致宕机或性能下降。
- 认证鉴权:网关层可以验证用户身份并确保其有足够的权限来执行所需的操作。
- 数据加密解密:网关层可以使用加密算法来保护数据的机密性,防止敏感信息在传输过程中被窃取。
- 协议转换:网关层可以将不同的协议转换成系统内部所使用的协议,以便不同系统之间进行通信。
- 日志记录:网关层可以记录请求和响应的详细信息,以便跟踪问题和进行审计。
十三、Eureka的服务注册过程
Eureka 是 Netflix 开发的服务发现框架,是 Spring Cloud 微服务架构中的核心组件之一。Eureka 的注册过程主要包括以下几个步骤:
- 服务注册(Service Registration):
- 当一个微服务实例启动后,它会向 Eureka Server 发送一个注册请求,包含自身的信息(如IP地址、端口号、服务名称等)。
- Eureka Server 接收到注册请求后,会将该实例的信息存储在它的注册表中。
- 获取注册信息(Fetching Registry Information):
- 服务消费者(或其它服务)会从 Eureka Server 获取注册表的信息,以此来了解可用的服务实例。
- 心跳机制(Heartbeat Mechanism):
- 已注册的服务实例会定期发送心跳(默认周期为30秒)给 Eureka Server,以表明其仍然处于活动状态。
- 如果 Eureka Server 在一定时间内(默认为90秒)未收到心跳,则会将该实例从注册表中移除。
- 服务下线(Service Unregistration):
- 当微服务实例准备下线时,它会向 Eureka Server 发送一个下线请求,告知服务器将其从注册表中移除。
- Eureka Server 集群(Eureka Server Cluster):
- 在 Eureka Server 集群模式下,各个 Eureka Server 实例之间会相互注册,并且同步注册表信息,以此来提供高可用性。
- 自我保护模式(Self Preservation Mode):
- Eureka Server 提供自我保护机制,当网络分区或其他异常情况发生时,Eureka Server 会进入自我保护模式,防止因网络问题导致的服务下线。
- 实例信息缓存(Instance Info Caching):
- Eureka Client 会缓存从 Eureka Server 获取的实例信息,即使 Eureka Server 不可用,Eureka Client 仍然可以使用缓存中的信息。
- 服务提供者与消费者(Service Provider & Consumer):
- 服务提供者(Provider)在 Eureka Server 注册后,服务消费者(Consumer)就可以通过 Eureka Server 发现服务提供者,并进行远程调用。
十四、什么是降级熔断?
熔断降级是保护系统的一种手段。当前互联网系统一般都是分布式部署的。而分布式系统中偶尔会出现某个基础服务不可用,最终导致整个系统不可用的情况, 这种现象被称为服务雪崩效应。
比如分布式调用链路A->B->C…,下图所示:
如果服务C出现问题,比如是因为慢SQL
导致调用缓慢,那将导致B也会延迟,从而A也会延迟。堵住的A请求会消耗占用系统的线程、IO、CPU等资源。当请求A的服务越来越多,占用计算机的资源也越来越多,最终会导致系统瓶颈出现,造成其他的请求同样不可用,最后导致业务系统崩溃。
为了应对服务雪崩, 常见的做法是熔断和降级。最简单是加开关控制,当下游系统出问题时,开关打开降级,不再调用下游系统。还可以选用开源组件Hystrix
来支持。
十五、什么是限流?
在计算机网络中,限流就是控制网络接口发送或接收请求的速率,它可防止DoS攻击和限制Web爬虫。限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。
可以使用Guava
的RateLimiter
单机版限流,也可以使用Redis
分布式限流,还可以使用阿里开源组件sentinel
限流。
Redis 篇
一、什么是Redis
Redis
本质上是一个key-value类型的内存数据库,整个数据库通通加载在内存中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis
的性能非常出色,每秒可以处理超过10万次读写操作,是已知性能最快的key-value DB。Redis
的出色之处不仅仅是性能,Redis
最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像memcached只能保存1MB的数据。
二、什么数据可以缓存
- 不需要实时更新但有极其消耗数据库的数据。
- 需要定时更新,但更新频率不高的数据。
- 在某个时刻访问量极大而且更新也很频繁的数据,但是这种数据使用的缓存不能和普通缓存一样,这种缓存必须保证不丢失,否则会有很大问题。
三、Redis为什么不作为数据库
第一,数据结构为K/V形式,不提供关系,需要自己维护关系,非常麻烦。
第二,不支持事务,MULTI/EXEC/WATCH不算
第三,异步持久化,丢数据,改同步性能就没有了
第四,异步复制,丢数据,WAIT性能就没有了,还是会丢。
第五,不具备数据库所能提供的数据安全性保障。
四、Redis的单线程和多线程
Redis
是一个基于内存的高性能键值存储数据库(NoSQL),其使用单线程模型来处理所有的命令请求。这种设计是为了避免多线程所带来的线程切换和同步访问的开销,从而提高系统的性能和可伸缩性。
在Redis的单线程模型中
,所有的命令请求都是按顺序执行的,因此可以保证数据的一致性。此外,Redis还通过使用非阻塞的I/O多路复用技术来实现高效的网络通信和事件处理,使得即使是在单线程模型下,也能够支持高并发的请求处理。
使用单线程模型有以下优点
- 简化了并发控制,无需处理多线程并发访问共享数据的同步问题,避免了加锁、解锁、死锁等问题。
- 减少了线程切换开销,单线程模型下不会出现多线程之间的上下文切换,避免了多线程带来的性能开销。
- 实现了原子性的操作,单线程模型下能够原子性地执行命令,保证了数据的一致性。
然而,由于Redis是单线程的,因此在面对大量计算密集型的任务时可能会受到影响。为了解决这一问题,Redis 6.0引入了多线程模型,通过使用多个线程来处理不同的任务,提高了 Redis在多核CPU下的性能。
在多线程模型中
,Redis会将不同的任务分配给不同的线程处理,例如处理客户端请求的线程、持久化操作的线程等。这样可以更充分地利用多核CPU的性能,提高系统的并发处理能力。不过需要注意的是,多线程模型增加了线程间的同步和数据共享的复杂性,需要更加谨慎地处理并发访问的问题。
总结
总的来说,Redis的单线程模型简单高效,适用于大多数场景,并且通过多路复用等技术提高了处理能力。在面对大量计算密集型任务时,可以考虑使用Redis 6.0的多线程模型来获得更好的性能。
五、Redis高可用
Redis 的高可用性(High Availability,简称 HA)
是指在部分硬件故障或节点故障的情况下,Redis 依然能够对外提供服务,确保数据不会丢失,并且服务不会中断。
Redis 高可用性主要通过以下几种方式实现
- 主从复制:Redis 的主从复制功能允许将一个 Redis 服务器的数据自动同步到多个从服务器上。这样即使主服务器发生故障,从服务器中可以选举出新的主服务器,继续提供服务。
- 哨兵系统:Redis 哨兵系统 是 Redis 的高可用性解决方案,它监控 Redis 主从服务器,并且在主服务器进入故障状态时自动进行故障转移,将一个从服务器提升为新的主服务器,并更新配置以最小化服务中断时间。
- 集群模式:Redis 集群通过分片存储数据提供了数据的分布式存储方案,同时通过主从复制和自动故障转移功能,实现了高可用性。
- 持久化:虽然持久化主要用于数据的安全性,但它也间接提高了 Redis 的可用性。当系统故障时,持久化的数据可以被重新加载,以快速恢复服务。
- 数据丢失防护:Redis 提供了一些配置参数,如 min-slaves-to-write 和 min-slaves-max-lag,以减少因异步复制导致的数据丢失风险。
- 脑裂防护:哨兵系统 可以防止脑裂现象,即防止在网络分区情况下出现多个主服务器。脑裂可能导致数据不一致,通过 Sentinel 的监控和自动故障转移机制,可以减少脑裂带来的影响。
- 自动故障转移:Redis哨兵系统 支持自动故障转移,当检测到主节点宕机时,会自动将故障转移给健康的从节点,无需人工干预。
- 配置更新:在故障转移后,Redis哨兵系统 会更新系统的配置,让客户端能够连接到新的主节点,保持服务的连续性。
总结
通过这些机制,Redis 能够实现高可用性,减少系统不能提供服务的时间,提高服务的稳定性和可靠性。然而,需要注意的是,虽然 Redis 提供了高可用性解决方案,但在实际部署时还需要考虑其他因素,如网络环境、硬件资源、运维能力等,以确保整个系统达到预期的高可用性水平。
六、Redis的淘汰策略
Redis的淘汰策略
指的是在内存空间不足时,决定删除哪些key来释放空间的策略。Redis提供了几种淘汰策略,包括以下几种:
- volatile-lru:在设置了过期时间的key中,通过最近最少使用的方式淘汰;
- volatile-ttl:在设置了过期时间的key中,通过TTL(Time To Live,生存时间)淘汰,即删除剩余时间最短的key;
- volatile-random:在设置了过期时间的key中,随机淘汰;
- allkeys-lru:对所有key使用LRU算法淘汰;
- allkeys-random:随机淘汰所有key。
怎么启用淘汰机制?
启用淘汰策略,可以在Redis配置文件(redis.conf
)中设置maxmemory-policy
参数,指定所采用的淘汰策略。例如,可以配置为maxmemory-policy volatile-lru
来启用volatile-lru淘汰策略。当Redis内存达到最大限制时,会根据设定的淘汰策略自动删除一些key,以释放空间。
在项目实际中,如果需要启用Redis的淘汰机制,可以按照以下步骤进行操作:
7. 配置Redis:修改Redis的配置文件(redis.conf
),设置maxmemory-policy
参数,选择合适的淘汰策略。可以根据实际需求和业务特点选择合适的淘汰策略。
8. 监控内存使用:定期监控Redis的内存使用情况,确保内存不会超出设定的最大限制。可以使用Redis的监控工具或者第三方监控工具对内存使用进行监控。
9. 性能测试与调优:在启用淘汰机制后,进行性能测试和调优,评估不同淘汰策略对系统性能和稳定性的影响,根据结果进行调整和优化。
10. 持续优化:根据实际业务情况,持续进行淘汰策略的调整和优化,确保系统在存储空间有限的情况下能够高效地处理数据。
通过以上步骤,可以在项目中启用并优化Redis的淘汰机制,提高系统的稳定性和性能,同时有效地管理内存空间。
七、Redis的数据类型及使用场景
Redis根据不同的数据类型,拥有不同的使用场景。
Redis的数据类型有:String、Hash、List、Set、Sorted Set。
- 字符串String:是Redis最基本的数据类型,可以存储任何类型的数据,包括数字、文本、二进制数据等。字符串的最大长度为512MB。
a. 使用场景:
ⅰ. 使用场景是用于缓存数据
ⅱ. 作为计数器,以Redis的自增命令实现计数器功能
ⅲ. 分布式锁 - 哈希Hash:是一种键值对集合,它可以存储多个字段和值,适合存储对象类型的数据,比如用户信息、商品信息等。
a. 使用场景:
ⅰ. 存储对象,可以将对象的各个属性存储在哈希中,方便查询和修改
ⅱ. 缓存数据
ⅲ. 作为计数器,以哈希的Hincrby命令实现计数器功能。 - 列表List:是一种有序的字符串集合,可以存储多个字符串元素,列表支持从两端插入和删除元素,可以实现队列和栈等数据结构
a. 使用场景:
ⅰ. 消息队列,可以使用列表实现简单的消息队列
ⅱ. 排行榜,可以使用列表存储用户的得分,然后根据得分排序
ⅲ. 日志记录,可以使用列表存储日志信息,方便查询和分析 - 集合Set:是一种无序的字符串集合,可以存储多个字符串元素,集合支持交集、并集和差集等操作,可以实现简单的数据分析
a. 使用场景:
ⅰ. 去重:可以使用集合存储重复的数据,然后使用 SREM 命令删除重复数据。
ⅱ. 标签系统:可以使用集合存储文章的标签,然后根据标签查询相关文章。
ⅲ. 推荐系统:可以使用集合存储用户的喜好,然后根据相似度推荐相关内容。 - 有序集合ZSet:是一种有序的字符串集合,它可以存储多个字符串元素和对应的分值。有序集合支持按照分值排序,可以实现排行榜和数据分析等功能。
a. 使用场景:
ⅰ. 排行榜:可以使用有序集合存储用户的得分,然后根据得分排序。
ⅱ. 数据分析:可以使用有序集合存储数据的分值,然后根据分值查询相关数据。
ⅲ. 推荐系统:可以使用有序集合存储用户的喜好和权重,然后根据相似度推荐相关内容。 - Bitmaps(位图):Redis 的位图是基于 String 类型的,可以使用 String 类型命令进行操作,但位图提供了对位级别的操作,如 SETBIT 和 GETBIT。
a. 使用场景:
ⅰ. 实现计数器
ⅱ. 用户签到系统 - HyperLog:是一种用于基数统计的高级数据结构,它可以用来估算一个集合中唯一元素的数量,而不管这个集合有多大,内存使用量都是固定的。
a. 使用场景
ⅰ. 统计分析
ⅱ. 基数估算 - Geospatial(地理空间):从 Redis 3.2 版本开始,Redis 引入了对地理空间索引的支持,允许用户存储和查询地理位置信息。
- Streams:Redis 5 引入了新的数据结构 Stream,它是一种类似于 Kafka 的日志数据结构,可以用于消息队列、事件源、日志聚合等场景。
- Modules(模块):Redis 允许通过模块扩展其功能,这些模块可以提供新的数据结构和命令。
- Listpack:这是一种用于替代简单动态字符串(SDS)的新型数据结构,用于存储列表类型的数据,特别是在数据量不大时,它比 SDS 更节省内存。
- Quicklist:这是 List 数据结构在 Redis 3.2 之后的新实现,它是一个双向链表,链表中的每个元素是一个压缩列表,用于存储大量数据。
- Intset:有序集合(Sorted Set)在数据量不大且所有分数都是整数时,会使用一种叫做整数集合(Intset)的数据结构来存储。
- Ziplist:这是一种紧凑的列表编码,用于存储少量元素的列表或哈希类型,它比使用双向链表更节省内存。
八、SDS 动态字符串原理
Redis的String底层数据结构实现并没有直接使用C语言中的字符串,为了方便扩展,Redis定义了一个结构用来存储字符串,即简单动态字符串SDS(Simple Dynamic String),并将SDS用作Redis的默认字符串。
优势
- 获取字符串长度的时间复杂度为1,有len字段的存在,无需像C结构一样遍历计数。
- 不会造成缓冲区溢出,有free字段的存在,让SDS在执行前可以判断并分配足够空间给程序。
- 修改字符串长度N次最多需要执行N次内存重分配,有free字段的存在,使SDS有了空间预分配和惰性释放的能力。
- 可以保存文本或二进制数据,有了len字段,准确了标识了数据长度,不需担心被中间的 ‘\0’ 截断。
- 可以使用以一部分<string.h>库中的函数。
九、ZSet的底层实现
在Redis中,Zset(有序集合)是一种以有序方式存储的数据结构,它的底层是通过跳跃表(Skip List)和哈希表(Hash Table)来实现的。
在Zset中,每个元素都包含了一个值和一个分值,跳跃表中按照分值对元素进行排序,哈希表中存储了每个元素值到其分值的映射关系。通过这两种数据结构的配合,Redis实现了有序集合Zset的底层存储和操作。
通过跳跃表和哈希表的组合,Zset实现了高效的有序存储和快速的元素查找操作,并且保持了较低的时间复杂度。这种设计使得Zset非常适合用于范围查询和有序数据的处理。
跳跃表 Skip List
跳跃表是一种带有多层索引的数据结构,可以在O(log N)的时间复杂度内进行查找、插入和删除操作。跳跃表通过层级索引来加速对底层数据的访问,每一层索引节点的数量都在逐层递减,最底层包含了所有的元素。在Zset中,跳跃表用于实现元素的有序存储和快速的查找操作。
哈希表 Hash Table
哈希表在Redis中作为跳跃表的辅助结构,用于存储元素到分值(score)的映射关系。通过哈希表,可以以O(1)的时间复杂度快速查找元素的分值以及判断元素是否存在。
十、Redis热点数据
比如说Mysql里有2000W数据,Redis只存了20W的数据,为了确保Redis中只存储热点数据,可以采用以下几种策略:
- 内存淘汰策略:利用Redis的内存淘汰机制,比如使用allkeys-lru策略,它会在内存不足以容纳新数据时,淘汰掉最近最少使用的数据,从而保留那些频繁访问的数据。
- 访问频率调整:在应用程序层面,可以追踪数据的访问频率,并将访问频率高的数据缓存到Redis中。如果某个数据的访问频率增加,可以动态地将其缓存时间延长。
3.数据结构优化:使用Redis的数据结构,如有序集合(Sorted Sets)来存储数据的热度信息,自动根据热度进行排序,并淘汰那些访问量低的数据。 - 时间窗口的缓存淘汰策略:在设定的时间窗口内(如1小时内),统计每个数据项的访问次数,超过预设阈值的数据将被缓存或延长缓存时间,低于阈值的数据则减少缓存时间或从缓存中移除。
- 内存大小限制:计算20万条数据大概占用的内存大小,并设置Redis的内存限制,这样Redis只能加载和存储热数据。
- 主键ID记录:如果内存大小难以精确计算,可以只记录热点数据的主键ID,这些ID通常是定长的,便于计算和控制内存使用。
- LFU策略的运用:从Redis 4.0开始,可以使用近似LFU(Least Frequently Used)淘汰策略,如volatile-lfu或allkeys-lfu,这样Redis能够根据数据访问频率自动进行淘汰决策。
- 热点数据定义与识别:定义何为热点数据,并通过业务日志、请求统计和系统性能监控工具分析用户行为数据,识别出哪些是热点数据。
十一、大批请求访问到Redis
当项目中出现大批量请求访问Redis时,可能会造成Redis服务器压力过大,影响系统的性能和稳定性。为了解决这个问题,可以考虑以下几种方法:
- 使用缓存策略:采用有效的缓存策略,对Redis中的数据进行合理的缓存和过期设置,避免频繁读写相同的数据。可以根据业务需求设置合适的缓存时间,以降低请求对Redis的频繁访问。
- 数据预热:对于热点数据,可以在系统启动时预先加载到Redis中,避免在高峰期才进行大量请求访问造成的压力。这样可以在系统稳定期间预先加载数据,减少高峰期对Redis的请求压力。
- 增加缓存层:在Redis外增加一层缓存,如使用内存缓存(如Memcached)或分布式缓存(如Redis Cluster),将部分请求直接命中缓存,减轻Redis的压力。
- 使用分布式锁:当请求需要进行写操作时,可以使用分布式锁来保证数据一致性,防止多个请求同时对同一数据进行修改。
- 优化Redis配置:对Redis进行合理的配置优化,包括最大连接数、内存管理、持久化方式等,以提高Redis的性能和稳定性。
- 集群部署:考虑使用Redis集群部署,提高Redis的负载能力和扩展性,通过横向扩展增加Redis节点,提高整体处理能力。
总结
综上所述,有效地使用缓存策略、数据预热,增加缓存层、使用分布式锁,优化Redis配置以及集群部署等方法可以有效地减轻大批请求对Redis的压力。同时,根据具体情况也可以结合使用以上方法进行综合考虑和实际优化。
十二、假如流量大到redis承受不了
如果Redis承受不了大流量的请求,可以考虑以下方案来解决这个问题:
- 水平扩展:将Redis服务器设置为集群,通过水平扩展增加Redis节点的数量,以提高整体的负载能力和处理容量。这样可以分担请求流量,降低单个节点的压力。
- 缓存命中率优化:通过使用更高效的缓存策略、数据预热和缓存层优化等方法,提高缓存命中率,减少对数据库的请求,降低对Redis的访问流量。
- 限流:对请求进行限流,在高峰期对进入Redis的请求数量进行控制,避免请求过多导致Redis过载。可以使用令牌桶算法、漏桶算法等进行限流。
- 请求削峰:对请求进行削峰处理,使用消息队列等机制对请求进行排队和平滑处理,尽量避免突发的大流量请求对Redis的冲击。
- CDN缓存:对大量静态数据或热点数据可以考虑使用CDN缓存,将部分数据存储在CDN中,减少对Redis的访问。
- 多级缓存:在系统中使用多级缓存,使用内存缓存(如Memcached)或本地缓存等机制,对部分请求进行缓存处理。
十三、Redis的持久化
Redis的持久化方式包括RDB持久化和AOF持久化。
-
RDB持久化:将Redis的数据以快照的形式保存到硬盘上,通过定时或周期性触发。RDB持久化产生的文件是一个压缩过的二进制文件,可以在服务启动时通过加载该文件来还原数据。
-
AOF持久化:将Redis的写操作记录下来,以日志的方式追加到文件末尾(append-only file),在服务启动时通过重新执行这些写操作来还原数据。
RDB持久化的优点
适合大规模数据的备份和恢复,在恢复时对IO消耗较小,因为是直接写入快照。
在数据不是很频繁改动的情况下,对性能影响比较小。
RDB持久化的缺点
如果发生故障,可能造成数据的部分丢失。
在进行持久化时,需要fork出一个子进程,可能在数据较大时存在较大的性能开销。
AOF持久化的优点
数据的完整性更好,因为是通过记录写操作来还原数据。
可以保证在发生故障时不会丢失太多数据。
AOF持久化的缺点
文件较大时,恢复数据的效率可能较低。
在频繁写入的情况下,AOF文件可能会变得较大,对IO的消耗也会比较大。
十四、Redis的看门狗机制
Redis的看门狗机制指的是Redis中的自动故障检测与恢复机制,主要用于实现Redis的高可用性
。看门狗机制基于主从架构,通过主节点和从节点之间的心跳检测和自动故障转移,确保Redis系统在主节点故障时能够迅速实现自动故障转移,保证系统的持久性和可用性。
Redis看门狗机制的主要工作原理
- 心跳检测:
○ 主节点(Master)会定期向从节点(Slave)发送心跳信息,以确认从节点的状态。
○ 从节点接收到主节点的心跳请求后,通过响应心跳请求来确认自身是否存活。 - 自动故障转移:
○ 在发现主节点故障的情况下,Redis看门狗机制会自动将一个从节点晋升为新的主节点,从而确保Redis系统的持续可用性。
○ 晋升为主节点的从节点会自动断开与原主节点的连接,并接管数据存储和处理请求。 - 配置监控:
○ Redis看门狗机制可以通过配置选项来定义故障转移的行为和条件,例如故障转移的超时时间、节点的确认机制等。
通过上述机制和流程,Redis的看门狗机制实现了对Redis节点的状态监控和自动故障转移,以应对节点故障导致的服务中断,从而提高系统的可用性。
十五、Redis怎么保证数据一致性
- 双写策略:在更新数据库的同时更新Redis缓存,确保两者数据的一致性。
- 缓存失效策略:当数据库中的数据被更新时,可以设置缓存失效,在下一次读取时从数据库加载最新的数据。
- 延迟双删机制:在更新数据库后,先删除缓存,然后经过一小段时间后再次删除缓存,以减少因高并发导致的数据不一致性。
- 读写分离:将读操作和写操作分离,读操作优先从缓存中读取,写操作则直接更新数据库,通过缓存失效保证最终一致性。
- 分布式锁:使用分布式锁来避免并发写入时的数据不一致问题。
- 数据过期:设置合理的缓存过期时间,当缓存数据过期时,系统将从数据库中重新加载最新数据。
- 消息队列:使用消息队列处理缓存更新操作,确保即使在高负载下也能有序更新缓存。
十六、Redis集群如何保存数据,原理讲一下
Redis集群的数据存储原理是基于分片技术的,即将整个数据集划分为多个部分(称为槽或分片),每个槽都可以被集群中的一个节点所持有。槽的数量固定为16384个,每个节点负责其中一部分槽的数据存储和处理。
当客户端向Redis集群发送写入请求时,该请求会被路由到对应槽所在的节点上,并在该节点上执行。因此,每个节点只需维护自己所负责的槽的数据,而不需要关心整个集群内所有数据的状态。这种分片方式可以提高Redis集群的可伸缩性和容错性。