最近花了一个月的时间,将一些Java相关的面试题进行了汇总,以通俗易懂的语言进行了总结,希望对大家有帮助,后续会继续更新完善相关内容,欢迎各位补充指正!
目录
3. StringBuilder和StringBuffer的区别
15. 抽象方法是否可以被static修饰?是否可以被synchronized修饰?
16. String s = new String("a") + new String("b")创建了几个对象?
6. HashSet、LinkedHashSet 和 TreeSet 三者的异同
8. ConcurrentHashMap和HashTable的区别
11. Comparable和Comparator接口的区别
一、Java基础篇
1. 重载和重写的区别
重载:一个类中多个同名方法根据传入参数的不同来执行不同的逻辑
重写:子类对父类的允许访问的方法进行重新编写
区别:
-
重载:发生在编译期,并且发生在同一个类中,方法名必须相同,参数列表必须不同(包括参数类型,参数个数,参数顺序);返回值和访问修饰符可以不同。
-
重写:发生在运行期,发生在子类中,方法名,参数列表必须相同,并且子类方法的返回值类型要小于等于父类方法的返回值类型(补充:如果父类方法的返回类型为void或基本数据类型,则子类方法的返回类型不能修改);子类方法抛出的异常要小于等于父类方法抛出的异常;子类方法的访问修饰符要大于等于父类方法的访问修饰符。
2. 面向对象的三大特性
封装:封装是将一个对象的属性私有化,同时提供一些可以被外界访问的公有方法。
继承:通过继承一个类,可以使用该父类的功能,复用该父类的代码,并且可以新增新的属性和方法。
多态:一个引用变量指向具体哪个类的实例对象和该引用变量调用的方法具体是哪个类的方法,在编译期间无法确定,必须在运行期间才能确定。多态的两种形式:继承类和实现接口。
3. StringBuilder和StringBuffer的区别
可变性:StringBuilder和StringBuffer都继承自AbstractStringBuilder类,它们都没有使用final关键字修饰字符数组,所以StringBuilder和StringBuffer都是可变的,而String对象是不可变的。
线程安全性:StringBuilder是线程不安全的,其内部没有对方法加同步锁;而StringBuffer是线程安全的,其内部对方法加了同步锁。
性能:StringBuilder的性能比StringBuffer的性能要高10%~15%。
4. 接口和抽象类的区别
-
接口中的方法默认是public,并且接口中的方法只能是抽象方法;而抽象类中可以有非抽象方法。
-
接口中的变量必须是静态常量;而抽象类中的变量可以是任意类型的。
-
接口中的访问修饰符默认是public;而抽象类的访问修饰符可以是public,protected,default。(抽象方法就是为了重写而定义的,所以修饰符不能是private)
-
一个类可以实现多个接口,但只能继承一个抽象类。
-
从设计层面来看,抽象是对类的抽象,是一种模板设计;接口是对行为的抽象,是一种行为规范。
补充:
jdk8开始,接口可以有默认的实现方法和静态方法。
jdk9开始,接口可以有私有方法和私有静态方法。
5. 成员变量和局部变量的区别
-
语法形式:成员变量可以被public,private,static等关键字修饰;而局部变量不能被访问修饰符和static修饰,但二者都可以被final关键字修饰。
-
生存时间:成员变量是对象的一部分,它随着对象的创建而存在;而局部变量存在于方法中,它随着方法的调用而消失。(用final修饰的成员变量和局部变量都存放在堆中,而不是栈中)
-
赋值方面:成员变量在定义时可以不赋初值,则会默认赋值(如果被final修饰则必须显示地赋值);而局部变量在定义时必须赋初值。
6. hashCode()和equals()
hashCode()方法的作用是:获取哈希码,用于确定该对象在哈希表中的索引位置。hashCode()方法定义在Object类中。
-
下面以"HashSet如何检查重复"为例来说明为什么要有hashCode():
当把一个对象加入到HashSet()时,会先根据对象的hashCode值计算对象加入的位置,同时会与HashSet中其他已加入对象的hashCode值进行比较,如果不相同,则说明对象没有重复出现;如果有hashCode值相同的对象,则会调用equals()方法来判断hashCode值相同的对象是否真的相同,如果相同,则说明两个对象相同,则不会让该对象加入。如果不相同,则可以将该对象加入到HashSet中。
-
为什么重写equals方法时必须要重写hashCode()方法?
两个对象相同,则它们的hashCode值也相同;如果两个对象的hashCode值相同,但这两个对象不一定相同。所以如果重写了equals()方法,没有重写hashCode()方法,则一个类的两个对象无论如何都不会相同(即使这两个对象指向相同的数据)。
7. final关键字总结
final修饰的变量:final修饰的变量在定义时必须显示地赋值,并且赋值后不能再被改变。
final修饰的类:final修饰的类不能被继承,并且final类中的所有成员方法都默认为final方法。
final修饰的方法:final修饰的方法不能被重写。
8. 如何防止某些字段被序列化
Java中可以使用transient关键字来修饰变量,阻止变量被序列化和反序列化。并且transient关键字只能修饰变量,不能修饰方法和类。
另外,static修饰的变量也不会被序列化(这也说明了一个类在实现Serializable接口时指定的serialVersionUID被static修饰,所以它不会被序列化,在JVM序列化对象时会自动生成一个serialVersionUID,这时就会将我们自定义的serialVersionUID赋值给自动生成的serialVersionUID)。
9. 获取用键盘输入的两种方式
-
通过Scanner方法
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
-
通过BufferedReader方法
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
10. Java的异常处理
Exception和Error是Java异常处理的重要子类,它们都继承自同一个Throwable类。
Exception:程序本身可以处理的异常,通过try-catch块处理。Exception分为受检查异常(编译时异常)和不受检查异常(运行时异常)。受检查异常在编译期间必须处理,例如:FileNotFoundException、ClassNotFoundException、InterruptedException等;不受检查异常在编译期间可以不处理,例如:NullPointException、ClassCastException、ArrayIndexOutOfBoundsException等。
Error:程序本身无法处理的错误,JVM一般会终止线程。例如:OutOfMemoryError、StackOverFlowError
以及虚拟机运行错误(VirtualMachineError)等。
Throwable类常用方法:
-
public String getMessage()
:返回异常发生时的简要描述 -
public String toString()
:返回异常发生时的详细信息 -
public String getLocalizedMessage()
:返回异常对象的本地化信息,若Throwable的子类覆盖了该方法,则可以生成本地化信息;若Throwable的子类没有覆盖这个方法,则返回信息和getMessage()相同。 -
public void printStackTrace()
:在控制台上打印异常信息
try-catch-finally块:
try块:用于捕获异常,可以搭配0个或多个catch块,如果没有catch块,则必须跟一个finally块。
catch块:用于处理捕获到的异常。
finally块:无论异常是否被捕获或处理,finally块都会执行。
注意:当try块或catch块中有retrurn语句时,finally语句会在方法返回之前执行;当 try 语句和 finally 语句中都有 return 语句时,在⽅法返回之前,finally 语句的内容先被执⾏,并且 finally 语句的返回值将会覆盖原始的返回值。
在以下3种特殊情况中,finally块不会被执行:
-
在try块或finally块中调用
System.exit()
退出程序,finally块不会被执行。 -
程序所在的线程被终止
-
关闭CPU
11. Java的IO流
Java中IO流的分类:
-
按流的方向分:分为输入流和输出流。
-
按操作单元分:分为字节流和字符流。字节流用于处理音频文件、图片等;字符流用于处理文本文件。
-
按流的角色分:分为节点流和处理流。
IO流的4个抽象基类:
-
InputStream:字节输入流
-
OutputStream:字节输出流
-
Reader:字符输入流
-
Writer:字符输出流
Java中IO流的 40 多个类都是从以上 4 个抽象类基类中派生出来的。
12.深拷贝和浅拷贝
浅拷贝:对于基本数据类型,拷贝的是数据值;对于引用数据类型,拷贝的是对象的引用地址,新旧对象指向的是同一个内存地址,当修改其中一个对象的值,则另一个对象的值也会改变。
深拷贝:对于基本数据类型,拷贝的是数据值;对于引用数据类型,则会开辟一块新的内存空间,创建一个新的对象,并复制旧对象的内容,这两个对象指向不同的内存地址,当修改其中一个对象的值,另一个对象的值不会改变。
13. 如何实现克隆
-
实现Cloneable接口并重写Object类中的clone()方法。
-
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现深克隆。
-
使用第三方工具实现,比如Spring的BeanUtils类。
14.两种单例模式
1.饿汉式
public class Singleton{
private Singleton{}
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
2.懒汉式(线程安全)
public class Singleton{
private Singleton{}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null)
instance = new Singleton();
}
}
return instance;
}
}
15. 抽象方法是否可以被static修饰?是否可以被synchronized修饰?
都不能,因为抽象方法需要子类重写,而static修饰的方法不能被重写只能被隐藏,所以二者矛盾;synchronized和方法的实现有关,而抽象方法不能有实现,所以二者矛盾。
16. String s = new String("a") + new String("b")创建了几个对象?
创建了5个对象,分别为:
(1) new StringBuilder() (2) new String("a") (3) 字符串常量池中的"a" (4) new String("b") (5) 字符串常量池中的"b"
"+"拼接操作:首先会创建一个StringBuilder,进行拼接,最后调用toString()返回一个String对象。
强调一下,toString()的调用,不会在字符串常量池中生成字符串"ab",即拼接后的字符串不会在常量池中生成
17. throw和throws的区别
-
throw在方法体内使用;throws在方法声明上使用
-
throw抛出的是异常对象;throws抛出的是异常类
-
throw只能抛出一种异常对象;throws可以抛出多个异常类并用逗号分隔开
-
throw是手动抛出异常并且执行了throw语句一定会出现异常;throws表示了会出现异常的可能性,并不一定会出现异常
-
throw抛出异常由方法体内的语句来处理;throws抛出异常由该方法的调用者来处理
18. Java创建对象有几种方式
Java有四种方式创建对象:
-
使用new创建对象:
User u = new User();
-
使用反射方式创建对象:使用newInstance()方法创建。
User u = User.class.newInstance();
-
使用clone创建对象:实现Cloneable接口并重写Object类的clone()方法。
-
使用反序列化创建对象:调用ObjectInputStream类的readObject()方法。
二、集合篇
1. 集合和数组的区别
-
数组是固定长度的;集合是可变长度的。
-
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
-
数组中的元素必须是同一种数据类型;集合中的元素可以是不同数据类型。
2. List、Set、Map的区别
List:存储的元素是有序的,可重复的
Set:存储的元素是无序的,不重复的
Map:以键值对的形式存储,key是无序的不可重复的,value是无序的可重复的
3. ArrayList和LinkedList的区别
线程安全性:ArrayList和LinkedList都是线程不安全的。
数据结构:ArrayList底层使用的是对象数组存储数据的;LinkedList底层使用的是双向链表。
内存空间:LinkedList比ArrayList占用更多的内存,因为LinkedList中每个数据要存放其直接前驱和直接后继。
是否支持快速随机访问:ArrayList支持快速随机访问;LinkedList不支持快速随机访问,链表需要遍历到指定位置才能访问。
应用场景:对于频繁访问元素的情况可以使用ArrayList;对于频繁添加删除元素的情况使用LinkedList。
4. ArrayList的扩容机制
在JDK1.8中,通过无参构造方法创建ArrayList,此时是一个空数组,当真正进行添加元素操作时才会为数组分配内存,默认数组容量为10。之后添加元素时,会先判断数组容量是否满足,如果不满足,则会进行扩容,容量扩容为原来的1.5倍。
5. HashSet如何检查重复
当把一个对象加入到HashSet()时,会先根据对象的hashCode值计算对象加入的位置,同时会与HashSet中其他已加入对象的hashCode值进行比较,如果不相同,则说明对象没有重复出现;如果有hashCode值相同的对象,则会调用equals()方法来判断hashCode值相同的对象是否真的相同,如果相同,则说明两个对象相同,则不会让该对象加入。如果不相同,则可以将该对象加入到HashSet中。
6. HashSet、LinkedHashSet 和 TreeSet 三者的异同
-
HashSet:元素是无序的,底层使用HashMap存储元素,HashSet的值作为HashMap的key存储在HashMap中,是线程不安全的,并且集合中的元素可以有一个null。
-
LinkedHashSet:按照元素的添加顺序来访问元素,底层使用hash表+双向链表存储元素,是线程不安全的,并且集合中的元素可以有一个null。
-
TreeSet:按照元素的添加顺序来访问元素,底层使用红黑树存储元素,是线程不安全的,并且不能有null。排序的方式有自然排序和定制排序,自然排序使用compareTo(Object obj)方法来比较元素的大小关系进行升序排序;定制排序是实现Comparator接口的compare(T o1,T o2)方法进行排序。
7. HashMap和HashTable的区别
-
线程安全:HashMap是线程不安全的,HashTable是线程安全的。
-
效率:HashMap的效率比HashTable要高。
-
存储null值:HashTable的key和value可以是null值,HashTable的key和value不允许null值
-
数据结构:在JDK1.8以后,HashMap采用数组+链表/红黑树的结构,数组用来存储数据,而链表用来解决哈希冲突的问题。当链表长度大于8时,数组长度小于64时,会先进行数组的扩容,当数组长度大于64时,会将链表转化为红黑树,来提高查询速度。
-
初始容量和扩容机制:①创建时不指定初始容量,HashTable的初始容量为11,之后扩容时,将扩容为原来的2n+1;HashMap的初始容量为16,默认加载因子为0.75,当元素个数大于数组初始长度的0.75时,就会进行数组的扩容,扩容为原来的2倍,然后重新计算每个元素在数组中的位置,这个过程是十分消耗性能的。所以我们在能预知存储元素个数的情况下,创建HashMap时指定初始容量,就可以避免扩容的性能下降问题。②创建时指定初始容量,HashTable会使用我们指定的初始容量;HashMap会将容量扩容为2的幂次方。
8. ConcurrentHashMap和HashTable的区别
-
数据结构:在JDK1.7时,ConcurrentHashMap采用分段的Segment数组+HashEntry数组+链表的结构,Segment用来充当锁的角色,HashEntry数组用来存储键值对,当HashEntry中的数据进行修改时,需要先获取对应的Segment锁。在JDK1.8时,ConcurrentHashMap采用数组+链表/红黑树的结构,不允许有null,数组用来存储数据,而链表用来解决哈希冲突的问题。当链表长度大于8时,数组长度小于64时,会先进行数组的扩容,当数组长度大于64时,会将链表转化为红黑树,来提高查询速度。
-
线程安全方式:在JDK1.7时,ConcurrentHashMap采用分段锁的方式,对整个桶数组进行了分段分割,每一把锁只能锁其中的一部分数据,多个线程访问不同数据段的数据,就不会存在锁竞争,提高了并发效率。在JDK1.8时,取消了分段锁的概念,采用Node数组+链表+红黑树的结构,使用synchronized和CAS来保证线程安全。而HashTable采用全表锁的方式来保证线程安全的,因此锁竞争激烈,效率较低。
9. HashMap的扩容机制
创建时不指定初始容量,HashMap的初始容量为16,默认加载因子为0.75,当元素个数大于数组初始长度的0.75时,就会进行数组的扩容,扩容为原来的2倍,然后重新计算每个元素在数组中的位置,这个过程是十分消耗性能的。所以我们在能预知存储元素个数的情况下,创建HashMap时指定初始容量,就可以避免扩容的性能下降问题。
例如,预知存储的元素个数为1000,那么在创建HashMap时指定初始容量为2000,HashMap会自动将容量扩容为2的幂次方也就是2048,这个时候2000*0.75=1500>1000,此时元素个数小于加载因子,HashMap就不会进行扩容,因此提高了性能。
10. HashMap的实现原理
在JDK1.8中,HashMap底层的数据结构是数组+链表/红黑树,数组用来存储元素的,链表用来解决哈希冲突的,当链表长度大于8数组长度大于64时,链表会转化为红黑树,提高查询效率,从O(n)到O(log n)。
HashMap是基于Hash算法实现的,当调用put()方法向HashMap中添加元素时,会根据key的hash值来计算元素在数组中的位置,并且会和其他key的hash值进行比较,如果出现hash值相同的key,有两种情况:①hash值相同并且key相同,则会覆盖原来的值。②hash值相同但key不同(即出现哈希冲突),则会将当前的key-value添加到链表中,来解决哈希冲突。JDK1.8以前使用拉链法解决哈希冲突,JDK1.8使用链表转化为红黑树解决。
11. Comparable和Comparator接口的区别
-
Comparable只包含了一个compareTo()方法,可以对两个对象进行排序,返回负数,0,正数表示输入对象小于,等于,大于已经存在的对象。
-
Comparator包含了equals()和compare()两个方法,compare()可以对输入的两个对象进行排序,返回负数,0,正数表示第一个对象小于,等于,大于第二个对象;而equals()用来判断输入参数和当前comparator的排序结果是否相同。
三、SpringBoot面试题
1.SpringBoot优点
-
独立运行:SpringBoot内嵌了Tomcat,只需打包成jar包即可运行。
-
简化maven配置:SpringBoot提供了各种starter启动器,简化了maven的依赖导入和版本冲突。
-
简化配置:SpringBoot在配置时无需代码生成和无需xml配置。
-
自动配置:SpringBoot根据当前类路径下的类,jar包可以自动配置bean,添加到容器中。
-
应用监控:SpringBoot提供了一系列端点来监控服务和应用,做健康检测。
2.SpringBoot的核心注解
启动类上的注解@SpringBootApplication,是SpringBoot的核心注解,它由三个注解组成:
-
@SpringBootConfiguration:实现配置文件的功能
-
@EnableAutoConfiguration:开启自动配置功能
-
@ComponentScan:组件扫描功能
3.SpringBoot的核心配置文件是什么
在一般开发中,不经常使用bootstrap.properties文件,但在结合SpringCloud时,会使用到该配置文件,常用于加载一些远程配置文件。
SpringBoot的两个核心配置文件:
-
bootstrap:由父ApplicationContext加载,比application优先加载。一般在Nacos中使用到,并且bootstrap中配置的属性不能被覆盖。
-
application:由ApplicationContext加载,用于SpringBoot项目的自动化配置。
4.Spring Boot starters是什么
定义:SpringBoot-starters可以理解为启动器,starters包含了许多项目中需要使用到的相关依赖,简化maven配置。
命名:SpringBoot官方的启动器以spring-boot-starter开头命名的,而第三方启动器不能以该方式开头命名,一般命名为名称+spring-boot-starter,例如mybatis-spring-boot-starter。
分类:starters可以分为:应用类启动器(如spring-boot-starter-web:使用SpringMVC构建web应用)、生产类启动器(spring-boot-starter-actuator:提供生产环境)、技术类启动器(如spring-boot-starter-json:提供json数据的读写支持)。
5.SpringBoot内嵌哪些servlet容器
SpringBoot内嵌了三种servlet容器,分别为Tomcat、jetty、undertow,默认是Tomcat。
如果要在项目中使用jetty的话,需要在spring-boot-starter-web中使用<exclusion>标签移除tomcat依赖,并引入spring-boot-starter-jetty依赖即可。
6.SpringBoot常用的两种配置方式
SpringBoot支持properties和yaml配置方式,并且properties的优先级高于yaml。yaml配置的数据更加结构化,使用缩进表示层级关系。
7.SpringBoot读取配置文件的方式
常用的读取配置文件方式有四种:
-
使用@Value读取配置文件
通过@Value("${}")方式读取某个配置项,一般用于读取单个配置项。
-
使用@ConfigurationProperties(prefix="xx")读取配置文件
该方式的@ConfigurationProperties需要和注入容器的注解如@Configuration、@Component等一起使用,才能将该配置读取类注入到容器中,在其他地方使用时直接@Autowired即可。或者就是在某一个类上使用@EnableConfigurationProperties(aa.class)注解就可以直接将该配置读取类aa注入到容器中,使用时直接@Autowired即可。
该方式用于读取前缀prefix为xx的配置项,一般用于读取一组配置项。
-
使用Environment读取配置文件
Environment是一个读取配置文件的类,只需要使用@Autowired将Environment类注入到某个类中,然后调用该类的getProperty()方法就可以获取某个配置项。
-
使用@PropertySource读取配置文件
该方式用来读取某个配置文件,类上使用@PropertySource(value="classpath:application.properties")读取配置文件。
注意:该注解@PropertySource只能读取properties格式的配置文件,不能读取yml格式的配置文件。
8.SpringBoot的自动配置原理
1.什么是自动装配?
SpringBoot在启动时会扫描引入外部jar包中的
META-INT/Spring.factories
文件,将文件中定义的配置类加载到Spring容器中进行管理,就可以实现其提供的具体功能。
2.自动装配原理?
首先,SpringBoot的核心注解是@SpringBootApplication复合注解,由@SpringBootConfiguration、@ComponentScan、@EnableAutoConfiguration注解构成。
@SpringBootConfiguration:标明当前启动类是一个配置类
@ComponentScan:扫描标记为bean的组件并将其注入到Spring容器中
@EnableAutoConfiguration:用于开启自动配置功能,它是实现自动装配的核心注解。 该注解由两个注解构成:@AutoConfigurationPackage、@Import(AutoConfigurationImportSelector.class)。
@AutoConfigurationPackage:指定默认的包规则,它会扫描主程序类所在包及其子包下所有的组件并注入到Spring容器中。
@Import注解会导入AutoConfigurationImportSelector类,它是实现自动装配功能的核心类,用于加载自动装配类。它的底层实现是调用了这个类的getAutoConfigurationEntry()方法,负责加载配置类。然后又间接地调用了loadSpringFactories()方法,它会查找所有
META-INF/Spring.factories
位置的文件,该文件中配置了项目启动时要加载的自动配置类,在项目启动时默认会全部加载。但最终会根据SpringBoot的条件装配规则@Conditional注解实现按需加载配置类,也就是说,只有在满足条件时配置类才会加载并生效。
四、Spring面试题
1.什么是IOC
IOC:控制反转,即将开发人员手动创建对象的控制权,对象之间的依赖以及对象的生命周期交给Spring容器管理,实现控制反转,在需要用到的地方直接注入即可,降低代码的耦合性。
2.什么是AOP
AOP:即面向切面编程,将那些与业务无关,但又被业务模块所共同调用的公共代码抽取出来,在需要用到的地方直接使用即可,降低代码之间的耦合,提高程序的拓展性和可维护性。
AOP是基于动态代理的,有两种代理方式:
-
JDK动态代理:代理类和被代理类实现同一个接口,可以使用JDK动态代理。
-
CGLIB动态代理:被代理类没有实现接口,可以使用CGLIB动态代理,即创建被代理类的子类作为代理。
应用场景:权限控制,日志管理,事务管理等。
3.Spring中bean的作用域有几种
-
singleton:唯一bean实例,Spring中默认的bean是单例的
-
prototype:每次请求都会创建一个新的bean
-
request:每次HTTP请求生成一个新的bean,该bean只在当前request内有效
-
session:每次HTTP请求生成一个新的bean,该bean只在当前session内有效
-
global-session:全局session作用域(Spring5已经没有了)
4.Spring的单例bean存在线程安全问题吗
Spring中的单例bean存在线程安全问题,当有多个线程操作同一对象时,对该对象内的非静态成员变量进行写操作时,会存在线程安全问题。
解决方法:
-
在bean对象中尽量避免定义可变的变量
-
使用ThreadLocal,将共享数据保存到ThreadLocal中,为每个线程提供一个该共享数据的副本。
5.Spring框架中使用了哪些设计模式
-
工厂模式:Spring使用工厂模式通过
BeanFactory
、ApplicationContext
创建bean对象 -
代理模式:Spring AOP是基于动态代理实现的
-
单例模式:Spring中默认的Bean都是单例的
-
适配器模式:Spring AOP的通知或增强(Advice)使用了适配器模式,SpringMVC通过适配器模式来适配Controller
-
观察者模式:Spring中的事件驱动模型使用到了观察者模式
6.Spring管理事务的方式有几种
-
编程式事务:通过硬编码方式管理事务
-
声明式事务:通过配置文件的方式管理事务(推荐)
-
基于xml的声明式事务
-
基于注解的声明式事务
-
7.Spring事务的隔离级别有几种
TransactionDefinition接口定义了5种隔离级别的常量:
-
DEFAULT:使用后端数据库默认的隔离级别。MySQL默认的隔离级别是REPEATABLE_READ。
-
READ_UNCOMMITED:允许读取未提交的数据,可能会导致脏读、幻读和不可重复读。
-
READ_COMMITED:允许读取已提交的数据,可以解决脏读,但可能导致幻读和不可重复读。
-
REPEATABLE_READ:可重复读,即对同一数据的读取结果是一致的,除非该数据被当前事务本身所修改,可以解决脏读和不可重复读,但可能导致幻读。
-
SERIALIZABLE:可序列化,最高的隔离级别,可以解决脏读,幻读和不可重复读,完全服从ACID的隔离级别,所有事务依次逐个进行,因此会严重影响程序的性能,一般不会使用。
8.Spring事务的传播行为有几种
TransationDefinition接口定义了7种传播行为的常量:
支持当前事务的:
-
PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;否则,创建一个新的事务。
-
PROPAGATION_SUPPORTS:如果当前事务,则加入该事务;否则,以非事务方式运行。
-
PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;否则,抛出异常。
不支持当前事务的:
-
PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则将当前事务挂起。
-
PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则将当前事务挂起。
-
PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
嵌套事务:
-
PROPAGATION_NESTED:如果当前存在事务,则创建一个事务嵌套在当前事务内运行;否则,则等价于REQUIRED方式操作。
9.什么是@Transactional注解
@Transactional注解可以进行事务管理,当前方法出现异常时,会进行事务回滚,能够保证数据的一致性。
@Transactional可以标注在类或方法上。当@Transactional标注在类上时,当前类的所有public方法都具有事务功能,当@Transactional标注在方法上,则只有当前方法具有事务功能。
@Transactional默认是在发生运行时异常时进行回滚,如果想要事务在发生非运行时异常时也回滚的话,则只需要在@Transactional注解中加上rollbackFor=Exception.class属性即可。
10.SpringMVC工作流程
-
客户端发起请求到前端控制器DispatcherServlet
-
DispatcherServlet接收到请求后会调用HandlerMapping去解析出Handler
-
DispatcherServlet将解析出的Handler交给ControllerAdapter,该适配器会根据Handler去调用真正的Controller控制器去处理本次请求,并返回ModelAndView给DispatcherServlet
-
DispatcherServlet会将ModelAndView交给ViewResolver视图解析器,返回真正的视图View
-
DispatcherServlet将模型数据填充到视图中,进行视图渲染
-
最后将视图View展示到客户端
五、MySQL篇
1. MySQL存储引擎
show engines;
查看所有的存储引擎。show variables like '%storage_engine%'
查看默认存储引擎。
在5.5版本之前,MySQL默认的存储引擎是MyISAM,在5.5之后,MySQL默认的存储引擎是InnoDB。
InnoDB和MyISAM的区别:
-
是否支持行级锁:MyISAM只支持表级锁;InnoDB支持表级锁和行级锁,默认为行级锁。
-
是否支持事务:MyISAM不支持事务;InnoDB是事务性存储引擎,所以支持事务和回滚。InnoDB支持外键
-
是否支持安全恢复:MyISAM崩溃后无法安全恢复;InnoDB崩溃后能够安全恢复。
-
是否MVCC:MyISAM不支持MVCC;InnoDB支持MVCC,应对高并发事务,MVCC能够避免加锁操作,比单纯的加锁更高效,并且MVCC只能在
READ COMMITED
和REPEATABLE READ
两个隔离级别下使用,在快照读(普通读)的情况下可以解决幻读的问题。MVCC可以通过乐观锁和悲观锁的方式实现。
2. MySQL索引
定义:索引是一种排好序的快速查找数据结构。
优点:索引能够提高查询数据的速度,降低IO的成本。
缺点:索引会占用额外的物理空间,会降低表的增删改的效率,因为每次增删改时需要动态地维护索引。
创建索引的场景:①主键会自动创建唯一索引②频繁的作为查询条件的字段③查询中排序的,分组的字段。
不创建索引的场景:①表记录太少的字段②频繁增删改的字段③数据重复且分布均匀的字段,如性别。
索引的分类:
①普通索引:最基本的索引。没有任何限制,用于加速查询。
②唯一索引:索引列的值必须唯一,可以为null,一个表可以有多个唯一索引。
③主键索引:是一种特殊的唯一索引,不可以为null,一个表中只能有一个主键索引。
④组合索引:包含多个列的索引,使用组合索引要遵循最左前缀原则。
⑤全文索引:用于查找全文的关键字。只能为char,varchar,text列创建,经常和match against操作搭配使用。
索引的相关语句:
①创建索引:CREATE INDEX indexName ON mytable(column_name);
②添加索引:ALTER TABLE mytable ADD INDEX indexName(column_name);
③查看索引:SHOW INDEX FROM mytable;
④删除索引:DROP INDEX indexName ON mytable;
索引优化:
①遵循最左前缀原则,即使用组合索引,查询时从索引的最左前列开始并且不跳过索引中间的列。
②不在索引列上做计算,函数和类型转换的操作。
③存储引擎不能使用索引列中范围条件右边的列(范围条件右边的列全失效)
④尽量使用覆盖索引(索引列和查询列一致),减少select *
⑤使用!=或<>时会导致索引失效而转向全表扫描
⑥使用is null,is not null会导致索引失效
⑦使用like以通配符'%abc'开头时会导致索引失效而转向全表扫描
⑧查询条件的字段是字符串并且不加单引号,索引会失效
⑨少用or,否则索引会失效
3. MySQL事务
概念:事务是逻辑上的一组操作,要么都成功,要么都不成功。
四大特性:
-
原子性:事务是最小的执行单位,不可分割,要么都成功,要么都不成功。
-
一致性:事务执行前后,数据都保持一致,即每个事务对同一数据的读取结果是相同的。
-
隔离性:在并发环境下,事务之间不会相互干扰。即每个事务操作同一数据时都有自己的独立数据空间。
-
持久性:事务提交之后,对数据库中数据的改变是永久的,即使数据库发生故障也不会受到影响。
并发事务带来的问题:
在并发环境下,多个事务对同一数据进行操作时,会产生以下问题:
-
脏读:一个事务对数据进行了修改,在未提交到数据库之前,这时另一个事务读取了该数据,导致读取到的数据是“脏数据”,结果可能不是最终提交到数据库中的数据。
-
不可重复读:一个事务对于同一数据的多次读取结果是不同的,在该事务多次读取之间,另一个事务对数据进行了修改,导致该事务多次读取的数据结果不一致。
-
幻读:事务T1读取了几行数据,之后事务T2新增或删除了一些数据,事务T1再次读取时会发现多了或少了一些数据,产生幻读的问题。
事务的隔离级别:
-
READ-UNCOMMITTED(读取未提交):允许读取未提交的数据,会导致脏读,不可重复读和幻读问题。
-
READ-COMMITTED(读取已提交):允许读取已提交的数据,可以解决脏读,会导致不可重复读和幻读。
-
REPEATABLE-READ(可重复读):对同一数据的多次读取结果是相同的,除非该数据被自身事务所修改。可以解决脏读和不可重复读,会导致幻读。
-
SERIALIZABLE(可串行化):最高的隔离级别,要求所有事务必须逐个执行,可以解决脏读,不可重复读和幻读
4. MySQL锁机制
按锁的粒度分类:
-
表级锁:锁粒度最大,对当前的整张表加锁,并且开销小,加锁速度快,不会出现死锁。但锁冲突的概率大,并发度低。
-
行级锁:锁粒度最小,只对当前操作的行加锁,并且开销大,加锁速度慢,会出现死锁。但是大大减少了数据库操作的冲突,并发度高。行级锁按照使用方式可以分为共享锁和排他锁。
注意:InnoDB存储引擎是基于索引来完成行锁的,锁住的是索引而不是记录。如果不通过索引条件查询数据时,那么就会对表中的所有记录加锁,相当于表锁,所以考虑性能的话,应该对WHERE条件查询的字段加上索引。
按锁的类型分类:
-
共享锁(S锁):
定义:也称为读锁。即一个事务对数据加上了共享锁,则其他事务想要读取该数据时只能加共享锁,不能加排他锁。并且获取到共享锁的事务只能对该数据进行读取不能修改。
用法:
SELECT * FROM t_name WHERE ... LOCK IN SHARE MODE;
-
排他锁(X锁):
定义:也称为写锁。即一个事务对数据加上了排他锁,那么MySQL会对查询结果的每行都加上排他锁,则其他事务不能再加任何类型的锁,直到释放了排他锁。并且获取到排他锁的事务可以对数据进行读取和修改。
用法:
SELECT * FROM t_name WHERE ... FOR UPDATE;
注意:对于select语句,InnoDB默认不会加任何锁。需要我们手动加锁。
对于update、delete、insert语句,InnoDB会自动给涉及到的数据加上排他锁。
InnoDB存储引擎的锁算法:
-
Record lock:单个行记录上的锁。
-
Gap lock:间隙锁,锁定一个范围,不包括记录本身。
-
Next-key lock:即Gap lock+Record lock,锁定一个范围,包括记录本身。
相关规则:
① 如果不通过索引条件查询,则会对表中的所有记录加锁,相当于表锁,若考虑性能,则应对WHERE条件查询的字段都加上索引。
② InnoDB使用Next-key lock来解决幻读问题,并且InnoDB只在可重复读的隔离级别下使用该机制。
③ 当查询的索引是唯一索引时,会将Next-key lock降级为Record lock,即锁住当前索引本身,而不是范围。
5. 大表优化
当MySQL单表记录数过大时,CRUD的性能就会急剧下降,因此要进行一些优化措施:
-
限制数据的范围
在查询时限制数据的查询范围条件。比如说在查询用户的订单信息时,限制查询的范围在一个月内。
-
读/写分离
主数据库负责读数据,从数据库负责写数据,主从之间通过二进制日志文件(由主数据库产生)进行数据同步。
-
垂直分区
根据数据库表的相关性进行拆分,即对数据库表的列进行拆分,拆分成多张表。
优点:① 简化表结构,易于维护 ② 使得表中的列变少,查询时可以减少读取的数据块数,减少I/O次数。
缺点:① 主键会出现冗余,需要管理冗余列
② 会引起表连接join操作,可以在业务层进行join减少数据库压力
③ 事务处理更加复杂
-
水平分区
根据某种策略将数据分片进行存储,这样每片数据会分散到不同的表或库中,达到分布式的效果,能够支持非常大的数据量,即对数据库表的记录进行拆分,拆分成多张表。
优点:① 提高数据库的并发能力 ② 应用端改造减少
缺点:① 分片事务的一致性难以解决 ② 跨节点join性能较差,逻辑复杂
注意:分表仅仅解决了单表数据量过大的问题,表的数据还是在一台服务器上,对于提高MySQL并发能力没有太大作用,因此,要通过分库来解决。
6. 分库分表后,如何生成全局ID
-
UUID:使用UUID生成全局唯一ID,但是生成的ID过长,查询效率低。
-
数据库自增ID:这种方式生成的ID有序,需要独立部署数据库实例,成本较高。
-
Redis生成ID:使用Redis的自增策略increament生成全局唯一ID。
-
雪花算法生成ID:以时间戳+序列号的方式生成全局唯一ID。
-
美团的Leaf分布式ID生成系统:可以保证全局唯一性,趋势递增,单调递增,信息安全,但需要依赖关系数据库,Zookeeper中间件实现。链接:Leaf——美团点评分布式ID生成系统 - 美团技术团队 (meituan.com)
7. SQL优化的20条建议
-
减少
Select *
查询,尽量查询具体的字段 -
如果预知查询结果是一条或是最大/最小数据时,使用limit 1
a.使用limit可以限制查询的记录数,因此可以避免全表扫描,提高查询效率。
select age from user where name='jk' limit 1; //补充: limit 1 等价于 limit 0,1 即从第1条记录开始,限制查询1条
-
尽量避免在WHERE中使用or来连接条件
a.使用or会导致索引失效,因此查询有索引的字段时使用union all来连接条件
//在创建表时我给user_id创建了索引,age没有索引 //在查询时若使用or,则查询走到age字段时就会进行全表扫描,而mysql是有优化器的,所以此时会导致索引失效 //总结:查询带索引的字段时,使用union或union all来连接条件,索引不会失效。 说到这,谈一下union和union all的区别: union:对查询的多个结果集进行合并,并且会去掉重复行,会进行排序。 union all:对查询的多个结果集进行合并,不会去重,也不会排序,因此效率比union高。 注意:使用union/union all时,要保证select查询的列个数一致,列类型一致,而列名可以不同。 select name from user where user_id=101 union all select name from user where age>20;
-
使用like模糊查询时尽量避免以'%'开头
a.使用like通配符以%开头时,会导致索引失效
//使用 like '%abc' 会导致索引失效; select * from user where name like '%abc'; //使用 like 'abc%' 索引不会失效,还是会走索引的 select * from user where name like 'abc%';
-
尽量避免在索引列上使用函数
-
尽量避免在索引列上进行计算操作
-
使用inner join、left join、right join时,优先使用inner join,若使用left join时,左表数据结果尽量小
//反例 select * from t1 join left t2 on t1.id=t2.id where t1.id>3; //正例 select * from (select * from t1 where t1.id>3) t0 join left t2 on t0.id=t2.id; 这里我还是再补充一下吧: inner join:内连接,返回的是两个表中字段匹配的记录。 left join:左连接,返回左表中的所有记录,和右表中满足匹配条件的部分记录。 right join:右连接,返回右表中的所有记录,和左表中满足匹配条件的部分记录。
-
尽量在WHERE中使用!=或<>操作符
a. WHERE中使用!=或<>会导致索引失效
//反例 select id,name from user where age!=20; //正例 select id,name from user where age>20 union select id,name from user where age<20;
-
使用WHERE时限制查询数据的范围,避免返回多余的记录
-
使用组合索引时,注意索引列的顺序,遵循最左前缀原则
a.使用组合索引时,从索引列的最左前列开始查询,并且不跳过中间的列
// 首先呢我创建了一个组合索引: create index idx_userid_age on user(userId,age); //反例 此时索引失效,转为全表扫描 select * from user where age=20; //正例 遵循最左前缀原则,会按索引查询 select * from user where userId=10 and age=20;
-
如果插入数据过多时,考虑使用批量插入
-
避免创建冗余和重复索引
//反例 create index 'idx_userid' on mytable('userid'); create index 'idx_userid_age' on mytable('userid','age'); //正例 组合索引(A,B)相当于创建了(A),(A,B)两个索引 create index 'idx_userid_age' on mytable('userid','age');
-
查询时WHERE中避免使用is null或is not null
-
不要有超过5个以上的表连接
-
一个表的索引数一般在5个以内
-
尽量使用数值型字段,避免使用字符型字段
a. 字符型字段会降低查询和连接的性能,增加存储开销
-
对于数据重复多的字段如性别,不适合建立索引
-
尽量使用varchar代替char存储字段,节省存储空间
-
查询条件是字符串类型的字段,使用单引号括起来,避免其进行隐式的类型转换导致索引失效
-
写SQL语句时加上explain,分析SQL语句的性能