Java面试题汇总(持续更新!)

moon

前言

以下是我对Java高频面试题的一个总结,后面我会持续更新,希望对你有所帮助。
当然,如果有哪道题目我理解错误了,或者是你有更好的见解,非常欢迎你在评论区中留下宝贵的意见!

JavaSE基础

1. JDK、JRE、JVM分别是什么,有什么区别

  • JDK(Java Development Kit) : 是Java标准开发包,它提供了编译、运行Java所需要的各种工具和资源,包括Java编译器、Java运行环境、以及常用的类库等
  • JRE(Java Runtime Enviroment):Java的运行环境,它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件
  • JVM(Java Virtual Mechinal):Java虚拟机,是JRE的一部分,它用于编译执行字节码文件,是可运行Java字节码文件的虚拟计算机

区别

  • JDK用于开发,JRE用于运行Java程序,如果你只是为了运行一下Java程序的话,安装JRE即可,但是要开发Java程序的话,必须要安装JDK
  • JDK包含JRE,JDK、JRE中都有JVM
  • JVM是Java实现跨平台性的核心,它具有平台独立性能,它拥有类加载以及垃圾回收等机制。

2. 重载和重写的区别

  • 重载发生在同一类中,当同一类中的两个或多个方法,方法名相同,参数列表不同,就构成了重载
  • 重写发生在类的父子关系中,当一个类继承另一个类或者实现了某一个接口,就可以重写该类的非私有方法,从而实现功能的拓展;重写要求方法名、参数列表相同

3. ==和equals的区别是什么

==对于基本类型来说就是值的比较,对于引用类型来说就是引用的比较。

而equals默认情况下是引用的比较,只是因为有很多类都重写了equals方法,比如String、Integer等类都把它变成了值的比较。

4. 什么是hashCode()

hashCode()方法作用获取一个哈希码,即散列码,它实际上是一个int整数。哈希码可以确定该对象在哈希表中的索引位置。hashCode的定义在Object类中,所以java中任何类都包含了hashCode函数。

散列表其实就是用来存放键值对(key-value),我们可以通过key快速检索出对应的value。其中就使用了散列码。

5. hashCode与equals有什么关系

要搞清楚他们之间的关系,我们可以从hash表的结构出发,其实hash表是一个数组+链表或红黑树的结构,Java的集合容器就有使用到这种数据结构的,然后在我们要存储对象到容器时可以先通过hashCode方法返回了一个hash值,这个hash与数组长度取余之后就能获取到该对象的存放在数组结构的一个对应的下标,当两个对象有相同的hash值的时候,说明存放的数组下标是一样的(也就是产生了哈希冲突),那么此时我们就需要通过equals方法判断这两个对象是不是相同的对象,如果不是则插入在链表中,所以我们可以得下面结论:

  1. 当两个对象相同时,它们的hash值也相同,它们之间的互相调用equals方法结果为true
  2. 当两个对象有相同的hashCode的值,那么它们也不一定相等。

补充:

对于HashSet存放的自定义类型来说,如果是同一个对象会插入失败

对于HashMap存放的自定义类型来说,如果是同一个对象会覆盖掉原来的value

6. 为什么重写equals方法必须重写hashCode方法呢

首先在java的一些容器中,是不允许存放两个相同的对象的,如果对象相同会覆盖掉,而在散列表中我们存放对象时,会首先判断两个对象的hash值是否相同,如果相同再通过equals方法来判断是否为相同对象,如果是相同对象就会在散列表中覆盖掉,否则就插入到相关链表中,那么如果我们只重写了equals方法,而不重写hashCode方法,那么那么相同对象的值就无法覆盖了。

7. 面向对象的特征

面向对象主要有三大特征:封装性、继承、多态

  1. 封装性:封装性指的是我们在定义对象时,尽可能的把对象状态信息隐藏起来,只提供有限的接口的方法给外界交互使用,从而避免外界对对象内部属性进行破坏。

  2. 继承:继承其实就是一种能力,当一个类A继承了另外一个类B时,A类就可以使用B类公开的所有的属性和方法,并且可以在不修改父类的前提下对其功能进行拓展。当然,如果B类存在用private修饰的属性或方法,A类是无法使用的,这就是封装性的体现。

  3. 多态:多态是指同一对象的不同形式的展现,比如说我们到宠物店跟店说“我要一只宠物”,那么店员就可以给我一只小猫、小狗、或者小蜥蜴等等,那么此时我们所说的宠物对象就是多态一种展现了。

    多态是怎么形成的

    其实java的引用变量有两种类型,一种是编译时类型、一种是运行时类型,而编译类型由引用变量在定义时的声明所决定,运行时类型由实际赋值给引用变量的类型所决定,当编译时类型与运行时类型不一样时,就会产生所谓的多态

8. 接口和抽象类的共同点和区别

共同点:

  • 都不能被实例化
  • 都可以包含抽象方法
  • 都可以有默认实现的方法(在JDK1.8之后才可以用default定义接口中的默认方法的)

区别:

  • 接口用于对类的行为进行约束,一个类实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系
  • 一个类只能继承一个类,但是可以实现多个接口
  • 抽象类中有构造方法,接口中没有构造方法
  • 抽象类中的成员变量默认是default,可以被子类重新定义赋值,接口中的成员变量是常量,不能被修改且必须要有初始值

9. java中操作字符串的类有哪些?他们之间有什么区别

操作字符串的类有主要有String、StringBuffer、StringBuilder三种。

在jdk1.8及以前String、StringBuffer、StringBuilder底层都是char数组。

在jdk1.9及以后,三者的底层都是byte数组。

使用byte[]还能存放中文的原因是因为他们的底层会有一个coder的值,该值可以是LATIN1UTF16的一种,所以能存放中文字符

因为String类是由Final修饰的,所以String类的长度是不会变化,每当我们新建一个String类时都需要new一个对象,让指针指向新的对象。而StringBuffer、StringBuilder都是继承于AbstractStringBuilder,而AbstractStringBuilder底层是使用一个可变数组来存放字符串的,所以他们存放的字符串内容是可变的。而且存在扩容机制。

扩容机制:

因为他们都是继承了AbstractStirngBuilder类,而该类有一个方法会检测当前字符串类的byte的容量是否足够来存放新的字符串,如果不能,进行扩容

扩容的逻辑就是先判断是字符串是否含有中文,在根据相应的一个处理得到一个新的byte数组,最后通过Arrays.copyOf方法将原来byte数组的值复制到新的byte值当中去。

然后StringBuffer和StringBuilder之间是有线程是否安全的区别的。

StringBuffer因为加了同步锁synchronized所以线程安全,因为synchronized同步锁会有一个获取锁和释放锁的操作,所以StringBuffer的效率更低。

为什么StringBuffer加了锁就效率更低呢?

因为synchronized锁其实是会有四种状态的升级和优化的。分别是:无锁、偏向锁、轻量级锁、重量级锁四种

偏向锁就是当一段同步代码或者是资源被同一个线程多次访问的时候,那么该线程就会自动获取锁,降低获取锁的代价。

而当锁是偏向锁时,如果有其他线程访问该锁时,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式来获取锁,不会阻塞,提高性能。

当前锁是轻量级锁时,另外一个线程不会一直自旋,当其自旋到一定次数后,轻量级锁会升级为重量级锁,其他线程进入阻塞状态,性能降低。因为线程自旋需要消耗cpu性能,所以自旋到一定次数后轻量级锁会膨胀成重量级锁。

StringBuilde因为没有加锁,所以效率更高。

10. String为什么要设计成不可变的?

String设计成不可变的原因主要有以下四点:

  1. 便于实现字符串池(String pool)

    因为在实际开发中,我们会大量的使用String常量,如果每次声明一个String类型都创建一个String对象的话,那将会造成极大的空间资源浪费。所以Java提出了String pool的概念,并且在堆中开放了一块专门用来存放String常量的字符串常量池(String Pool)。当我们初始化一个String变量时,JVM会先去查找字符串常量池是否已经存在该字符串了,如果存在则直接引用,如果不存在则新建一个字符串对象。

    如果String是可以变的,那么一个字符串变量改变了他的值,都会指向一个新的String对象了,而String Pool也无法实现了

  2. 线程安全

    在多线程环境下如果对同一个资源同时进行写操作时会引发一个线程安全的问题,而String设计成了不可变,确保了String对象不能被写,所以保证了多线程的安全。

  3. 避免安全问题

    在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。

  4. 加快字符串处理速度

    由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。

总体来说,String不可变的原因要包括 设计考虑,效率优化,以及安全性这三大方面。

11. 深克隆与浅克隆的区别(深拷贝和浅拷贝)

浅克隆(默认)

  • 拷贝出的新对象,与原对象中的数据一模一样(引用类型拷贝的是地址)

    shallow-copy

深克隆(避免新对象修改其他对象内容时,影响原对象的对象内容)

  • 对象中基本类型的数据直接拷贝

  • 对象中的字符串数据拷贝的还是地址

  • 对象中还包含的其他对象,不会拷贝地址,会创建新对象

    deep-copy

12. Java 是编译执行的语言,还是解释执行的语言?

Java即是编译型的,也是解释型语言,总的来说Java更接近解释型语言

  • 可以说它是编译型的。因为所有的Java代码都是要编译的,Java不经过编译就什么用都没有。同时围绕JVM的效率问题,会涉及一些如JIT、AOT等优化技术,例如JIT技术,会将热点代码编异成机器码。而AOT技术,是在运行前,通过工具直接将字节码转换为机器码
  • 可以说它是解释型的。因为Java代码编译后不能直接运行,它是解释运行在JVM上的,所以它是解释运行的。

13. jdk1.8的新特性

  1. Lambda表达式

    Lambda 允许把函数作为一个方法的参数

  2. 方法引用

    方法引用允许直接引用已有Java类或对象的方法或构造方法

  3. 函数式接口

    只有一个抽象方法的接口就叫函数式接口,函数式接口可以被隐式转换为Lambda表达式。通常函数式接口上回添加一个@FunctionalInterface注解

  4. 接口允许定义默认方法和静态方法

    从JDK8开始,允许接口存在一个或多个默认非抽象方法和静态方法

  5. Stream API(Stream流)

    Stream API把真正的函数式编程风格引入到Java中。

    这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选、排序、聚合等

  6. 日期/时间类改进

    在JDK8之前,我们JDK提供的日期处理类非常不方便,我们在处理的时候通常是使用第三方工具包,比如commons-lang 包等。

    在JDK8之后,因为LocalDateTime的出现,日期时间的创建、比较、调整、格式化、时间间隔等操作都比以往简单很多。

  7. Optional 类

    Optional 类是一个可以为null的容器对象。如果值存在则 isPresent() 方法会返回true,调用 get() 方法会返回该对象

  8. Java8 Base64实现

    Java8内置了 Base64 编码的编码器和解码器

14. Final有什么作用

Final是一个关键字修饰符,被Final修饰的类或成员变量有以下特点

  • 被final修饰的类不可以被继承

  • 被final修饰的方法不可以被重写

  • 被final修饰的变量不可以被改变

    被final修饰的变量不可变的是变量的引用,而不是引用指向的内容,引用指向的内容时可以改变的

15. 什么是反射机制

Java反射机制是在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法的信息;对于任意一个对象,都能够调用它的任意一个方法和属性;

在Java中,这种动态获取类的信息以及动态调用对象的方法的功能被称为反射机制

16. 反射机制的优缺点

优点:运行期类型的判断,动态加载类,提高代码灵活度

缺点:性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的Java代码要慢很多

JavaSE进阶-IO

1. BIO NIO AIO有什么区别?

  • BIO:Block IO 同步阻塞式IO,就是我们平常使用的传统ID,它的特点是模式简单使用方便。但是并发处理能力低。

  • NIO:New IO 同步非阻塞IO,是传统IO的升级,客户端和服务端通过Channel(通道)通讯,实现了多路复用

    IO多路复用模型

    该模型是有一个多路复用器,也就是一个选择器(Selector),通过这个选择器,我们只需要一个线程便可以管理多个客户端连接。当用户数据到了之后才会为其服务

  • AIO:Asynchronous IO 是NIO的升级,也叫NIO2,实现了一部非阻塞IO,一部IO的操作基于事件和回调机制

BIO_NIO_AIO

JavaSE进阶-异常

image-20230611074554345

Throwable是所有Java程序中错误处理的父类,他有两种子类:Error和Exception

1. Error 和 Exception 区别是什么

  • Error

    表示由JVM所侦测到的无法预期的错误,由于这是属于JVM层次的严重错误,导致JVM无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,最多只是显示错误信息

  • Exception: 程序本身可以处理的异常,可以通过 catch 来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。Exception 又可以分为运行时异常(RuntimeException, 又叫非受检查异常)和一般异常(又叫受检查异常) 。

运行时异常(非受检查异常)和一般异常(受检查异常)的区别

运行时异常:包括 RunTimeException 类及其子类,表示JVM在运行期间可能出现的异常。Java编译器不会检查运行时异常。

常见的运行时异常有:

NullPointException(空指针)NumberFormatException(字符串转换为数字)IndexOutOfBoundsException(数组越界)ClassCastException(类转换异常)ArrayStoreException(数据存储异常,操作数组时类型不一致)

一般异常(受检查异常):是Exception中除 RuntiemException 及其子类紫外的异常。 Java编译器会检查一般异常(受检查异常)。

常见的一般异常有:

IO相关的异常、ClassNotFoundExceptionSQLException

两者之间的区别: 它们之间的区别就是是否在编译阶段就能检查到。

JavaSE进阶-集合

1. Array 和 ArrayList 有什么区别?

  • Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。

  • Array 大小是固定的,ArrayList 的大小是动态变化的。

  • ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。

2. 常见的集合类有哪些

Java的集合容器有单列集合和双列集合两种,单列集合指的是Collection接口下的派生类:List、Set、Queue(JDK5 新增的队列);双列集合则是Map接口下的派生类。

注意:Collection是一个接口,Collections是一个工具类,Map不是Collection的子接口

在集合体系中,List集合代表了可重复集合,他们是有索引的;Set集合代表无序不可重复的集合,只能根据元素本身来访问(无索引)。Queue集合是一个队列集合。

Map集合时可以存放key-value键值对的集合,他的key是不可重复的,value是可以重复的。

List集合常用的有ArrayList、LinkedList

Set集合中常用的有HashSet、TreeSet

Queue集合常用的有ArrayQueue

Map集合中常用的有HashMap、TreeMap、ConcurrentHashMap

3. ArrayList与LinkedList的异同

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  • 底层数据结构: Arraylist 底层使用的是Object数组(动态数组);LinkedList 底层使用的是双向循环链表数据结构;
  • 插入和删除是否受元素位置的影响: ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而ArrayList 实现了RandmoAccess 接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

4. ArrayList的扩容机制

因为ArrayList底层维护的是一个数组,所以ArrayList的扩容机制是基于Arrays.copyOf方法实现的。

当我们用默认的无参构造方法创建ArrayList集合的时候,ArrayList的初始长度为0。当我们向集合添加第一个元素的时候,ArrayList集合的长度为10;

当我们向集合添加元素时都会有一个判断,就是查看当前容量是否可以继续添加元素,如果集合的容量够就添加到集合中,如果容量不够则触发扩容机制,扩容的大小为原集合容量的1.5倍。

1.5倍是因为新的集合容量为原集合容量其右移一位之后的和

1
int newCapacity = oldCapacity + (oldCapacity >> 1);

当我们使用有参构造去创建ArrayList集合时,扩容机制也与上面的一样,不过需要注意的是他扩容的大小为你传进去的参数的1.5倍

5. HashMap的底层数据结构是什么

在JDK1.8以前,HashMap底层维护的是一个”数组+链表“的结构,数组是HashMap的主体,链表是为了解决哈希冲突而存在的。

在JDK1.8之后,HashMap的底层变成了”数组+链表+红黑树“的数据结构了

在JDK1.8之前,如果我们使用HashMap存放大量数据时,会使HashMap的链表结构变得非常长,这样会导致我们的查询效率变慢,严重的影响了HashMap的性能。

因此,在JDK1.8之后对数据结构做了进一步的优化,引进了红黑树,链表和红黑树会在达到一定条件时会进行转换:

  • 当链表长度达到了8,且数组的长度达到64时才会发生转换
  • 因为在触发树化机制的时候,HashMap的底层会进行判断,如果当前数组的长度没有达到64,会优先对数组进行扩容,而不是进行树化,这样可以节省查询时间。

面试官也许还会问:

如你上面所说,为什么HashMap在JDK8之后不直接采取数组+红黑树的结构,而是使用数组+链表+红黑树的数据结构呢?

首先因为红黑树会在每次添加元素时进行一个左旋、右旋、变色等操作来保持平衡,链表则不会,因此链表插入数据的效率会比红黑树快。

所以在数据量很少的时候,链表的效率会比红黑树高;

但是当数据量多了之后,因为红黑树搜索的时间复杂度为O(log n),而链表的是O(n),因此选择红黑树可以增加的查询效率,但是新增的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

6. HashMap的扩容机制

  • HashMap底层维护了Node类型的数组table,默认为null,链表与红黑树是为了解决哈希冲突而存在的。
  • 当创建对象时,将加载因子(loadfactor)初始化为0.75;
  • 当添加key-value时,通过key的hash值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,则直接替换value如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
  • 第一次添加时,会将需要扩容的table容量扩容到16,临界值(threshold)为12
  • 如果需要再次扩容,会将需要扩容的table容量扩容到原来的2倍(32),临界值为原来的2倍(24) 依次类推。
  • 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(转换红黑树),否则任然采用数组扩容机制

HashSet的底层就是HashMap,唯一区别是HashSet的value值为一个常量,所以当同一hash值中的key相同时,会添加失败;而HashMap会替换掉key对应的value。

7. ConcurrentHashMap的实现原理是什么?

ConcurrentHashMap的底层数据结构

ConcurrentHashMap 在JDK1.7和JDK1.8的实现方式是不同的。

JDK1.7

ConcurrentHashMap在JDK1.7是由segmentHashEntry 组成的。**ConcurrentHashMap会把哈希桶切分成小数组(segment)**,每一个segment 都包含了n个HashEntry , 也就是我们HashMap的一个结构。

ConcurrentHashMap能实现线程安全其实就是在每一个segment 中加了一个可重入锁(也就是说每一段数据都会有锁),当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。

底层结构图如下图所示

JDK_7_ConcurrentHashMap

补充:

segmentConcurrentHashMap的一个内部类,它继承了ReentrantLock可重入锁,扮演锁的角色(构成了分段锁)。所以segment锁也是可重入的。

JDK1.8

在数据结构上,JDK1.8中ConcurrentHashMap选择了与HashMap相似的Node数组+链表+红黑树的数据结构,这样可以使ConcurrentHashMap的数据结构不再像JDK1.7那么的臃肿,使用起来更为方便;在锁的实现上,抛弃了原有的 Segment 分段锁,采用了CAS+Synchronized实现更加细粒度的锁

image-20230621195606507

在JDK8中,ConcurrentHashMap 将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说我们只需要在链表的头结点或者红黑树的根节点上加锁,就不会影响其他哈希桶数组元素的读写,从而实现了并发。

也许面试官还会问:为什么JDK8使用了Synchronized锁替换可重入锁ReentrantLock呢?

  • 在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
  • 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

JVM

1. 描述一下jvm加载.class文件的过程吗?(类加载过程)

因为Class文件需要加载到虚拟机中才能运行和使用,所以就有了类加载的过程

类在JVM中的生命周期主要有:加载、连接、初始化、使用、卸载五部分组成。

系统加载 Class 类型的文件主要三步:加载->连接->初始化

加载过程指的是JVM将经过javac编译的字节码文件加载到内存当中的过程。

连接阶段分别由验证、准备、解析三个阶段组成。

  • 验证阶段会对字节码文件的格式、元数据格式等进行验证,确保字节码文件没有损害JVM的内容,是保护JVM的一个阶段。

  • 准备阶段会为class文件的静态变量分配内存,并赋予初始值,此时的初始值指的是0

  • 解析阶段会将符号引用转换为直接引用

连接阶段完成之后会到初始化阶段,该阶段开始执行java中定义的代码,为变量赋予真正的初始值,指的是代码中程序员想要赋的值

类加载是由类加载器完成的,类加载器分别有:启动类加载器、扩展类加载器、系统类加载器、自定义加载器等

2. 什么是双亲委派模型,为什么要使用

双亲委派原则指的是类加载器在接收到类加载的一个请求时,不会立刻加载,会先委派该类的父类加载器去加载,只有当父类加载器在他的搜索范围内没有搜索到该类时,子类的加载器才会对该类进行加载。

这样做可以防止内存中出现多个相同的字节码文件;因为如果没有双亲委派模型的话,那么用户自己再定义了一个java.lang.String的类,就无法保证类的唯一性了。

3. 可达性算法

可达性算法的逻辑就是,在以一个GC Root为根节点的对象作为起点开始搜索,能遍历到的对象,也就是说该对象的引用还存在,就是可达的对象。而不能遍历到的对象,说明对象的引用已经不存在了,所以是不可达对象,也就是被GC回收的对象。

可以作为GC Roots的对象有哪些呢?

  • 在栈帧的局部变量引用的对象
  • 在本地方法区JNI引用的对象
  • 在方法区(元空间)的静态变量或常量引用的对象

4. 你能说一下垃圾回收机制吗?

首先我们要理解JVM的一个内存模型,因为JVM的内存模型是由栈、本地方法区、程序计算器、元空间、堆这5部分组成。

然后因为我们创建的对象,是存放在堆的,然后当我们一段代码执行完成之后,只有存放在栈的局部变量可以自动删除,而存放在堆的对象仍然存在,这时候就需要垃圾回收机制来回收堆的内存了。

然后垃圾回收的算法主要有三种

  • 标记清理法:就是先遍历一次堆里面的所有对象,把要删除的对象标记,然后再遍历一次,把第一次遍历标记的对象都删除掉,就完成了,但是有一个缺点,就是会存在内存碎片

    什么是内存碎片?

    内存碎片我是这样理解的,比如我们在堆中的两个不相连的位置中分别有一个1KB大小的对象,然后我们通过标记清理法把他们删除了,那么堆中就挤出来2KB的内存空间了,如果我们这个时候新建一个2KB大小的对象,是无法存放在堆中的,因为上面清理出来的2KB空间不是连续的,所以就会产生内存碎片问题

  • 标记-整理法:他跟标记清理法原理是差不多的,但是他会在删除对象后,把后面的对象往前推,这样子就解决了内存碎片的问题,但是由于每删一个对象都需要移动一次后面的对象,会导致性能会有所降低,而且这样做的代价太大了。

  • 复制法:复制法会把内存分成两块大小一样的内存,首先把其中一块内存通过GC ROOT标记,再把标记过后存活的对象复制到另一块内存中去,最后把存放垃圾对象的内存清理掉,缺点就是需要双倍的内存,太消耗内存了。

所以GC综合了上面三个算法,创建了一个分代收集的算法出来

分代收集算法就是先把内存分为young区跟old区两块,分别代表年轻代和老年代。

然后再young区那一块内存中,又分为E空间、S0空间、S1空间;

hotspot-heap-structure

简单的概括就是:

大部分情况,对象都会首先在 Eden 区域分配。

然后在一次GC之后,E空间和S0空间存活的对象会移到S1空间去,再回收E空间和S0空间的垃圾对象

在下一次GC完成后,E空间和S1空间存活的对象会移到S0空间去,然后回收E空间和S1空间的垃圾对象

每一次被标记存活的对象,都会有一个类似于age的变量,都会加一,当age>一个阈值时,他会晋升到老年代去。需要注意的是,大对象(需要大量内存的对象,例如集合、数组)会直接存放在老年代

Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的 50% 时(默认值是 50%,可以通过 -XX:TargetSurvivorRatio=percent 来设置),取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”

补充:垃圾回收机制中会触发的几种GC方式的区别以及详解

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC):收集整个 Java 堆和方法区。

rf-hotspot-vm-gc

并发编程

1. 你能说一下创建线程的方法有哪些吗

创建线程的方式有四种,分别是

  1. 继承Thread类并重写run方法创建线程,虽然实现简单,但是不可以继承其他类
  2. 实现Runnable接口并重写run方法方法。避免了单继承的局限性,编程更加灵活,很好的实现了解耦合
  3. 实现Callable接口并重写call方法,创建线程,通过该方法可以获取线程执行结果的返回值,并且可以抛出异常。
  4. 通过线程池创建。(使用java.util.concurrent.Executor接口)

2. Runnable接口和Callable接口的区别

Runnable接口的run方法没有返回值,Callable接口的call方法有返回值,并且支持泛型

Runnable接口的run方法只能抛出运行时异常,且其他异常只能在内部处理异常,而Callable接口可以抛出所有的异常,以及在内部处理。

3. sleep方法和yield方法有什么区别

  1. sleep方法不会考虑优先级问题,然后优先级低的线程也能拿到CPU资源,而yield方法会考虑优先级的问题,调用该方法只会给同等优先级或高优先级的线程CPU资源
  2. Sleep方法有声明InterruptedExcetion异常抛出,yield是没有异常抛出的
  3. sleep方法执行后线程会进入阻塞状态,而yield方法执行后会进入就绪状态
  4. sleep比yield具有更好的移植性

4. 线程中sleep和wait方法有什么区别

首先sleep()方法是线程类的一个静态方法,调用该方法可以让线程暂停指定的一段时间,暂停的线程会把CPU资源让出来给其他线程使用,但是对象的锁依然保存,所以当暂停时间到后,该线程会自动恢复到就绪状态。而wait方法是Object类一个方法,调用对象的wait方法可以让当前线程放弃对象的锁,即进入暂停状态,然后当前线程会进入到对象的等待池,知道对象调用notice方法时才能唤醒等待池中的线程进入等锁池,如果线程重新获得对象的锁就可以进入到就绪状态。

5. 线程的run方法和start方法有什么区别

调用start()方法可以让启动一个线程,并进入到就绪状态,而调用run()方法只是调用了一个对象的方法,并没有开启新的线程。

就比如说你在主线程中调用了run()方法,那么你要等run()方法执行完之后才会继续执行下面的代码,而调用了start()方法,会开启一条线程去执行Thread对象中对应的run方法,即使run方法没有执行完,主线程中还会执行start()下面的代码。

6. 线程有哪几种状态?(线程的生命周期)

Java线程在运行的生命周期中的指定时刻只可能处于下面6中不同状态的其中一个状态

  • NEW:线程初始状态,线程被创建出来但没有被调用start()方法的状态
  • RUNNABLE:运行状态,线程被调用了start()等待运行的状态。RUNNABLE状态细分一下会有一个RUNNING状态以及READY状态的
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,调用wait方法后的一个状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)
  • TIME_WAITING:超时等待状态,调用wait方法并且传入了一个时间之后的状态,可以在指定的时间后自行返回而不是想WAITING那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间转换。

Java-thread-status

7. 线程相关的基本方法都有哪些

线程相关的基本方法有6种,分别是wait、sleep、notifyAll、notify、join、yield

  1. wait方法(线程等待):

    线程调用wait方法并且没有传入参数时,会让该线程进入到一个WAITING状态,并且释放锁资源,直至被其他线程调用notifynotifyAll方法才会进入就绪状态重新竞争锁资源;如果调用wait方法并传入一个指定的时间,该线程会进入到TIMED_WAITING状态,如果指定超过的时间还没获取到锁资源,该线程就会直接返回到就绪状态,不会一直在等待通知

  2. sleep方法(线程睡眠)

    sleep方法会导致当前线程休眠,与wait方法不同的是sleep不会释放锁资源,然后sleep会导致线程进入TIMED_WAITING状态,当休眠时间过去之后,当前线程会自动唤醒并且继续执行。这个过程中其他线程处于阻塞状态

  3. yield方法(线程让步)

    yield方法会让当前线程让出CPU时间片(资源),同时让当前线程重新与其他线程竞争CPU时间片,一般来说优先级高的线程会比较容易获得CPU时间片。但不是绝对的,有一些操作系统对线程优先级不敏感。

  4. interrupt(线程中断):

    该方法可以中断一个线程,该方法会将线程中的一个标识符设置为true,表示中断该线程,但是线程中断之后并不会释放锁资源,需要结合wait方法的使用才会释放锁资源。这个过程中线程的状态不会发生改变

  5. join方法(等待其他线程终止):

    在当前线程调用另外一个线程的join()方法时,会让当前线程进入到阻塞状态,直到另一个线程执行结束,当前线程才会由阻塞状态转换为就绪状态并且重新竞争锁资源。

  6. notify方法(线程唤醒):

    Object类上的notify方法,可以唤醒在此对象监视器上等待的线程(可以理解为在当前锁对象外面等待的资源),如果所有的线程都在这个对象上等待,则会唤醒任意一个线程。

    在调用notify()方法之后,线程并不会立即释放该对象的锁。它会继续执行同步代码块,直到离开同步代码块或调用wait()方法时才会释放锁,从而使等待的线程有机会获取锁并执行。

    在使用wait()和notify()方法进行线程间通信时,需要确保等待线程和唤醒线程都使用相同的对象作为锁,以避免死锁等问题。

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

wait()方法与sleep()方法的区别主要有一下四点

  1. 它们来自不同的类

    wait()来自Object类

    sleep()来自Thread类

  2. 调用wait方法会释放锁资源,调用sleep方法不会释放锁资源

  3. wait方法必须要在同步代码块中使用,sleep方法可以在任何地方使用

  4. wait方法不需要捕获异常,sleep方法需要捕获异常

9. Java中常用的线程池有哪些

常用的线程池主要有6种,分别是:

  1. newFixedThreadPool:创建一个定长线程池(核心线程数与最大线程数都为指定参数的线程池),可以控制线程最大并发数,超出的线程会在队列中等待。
  2. newCacheThreadPool:创建一个可缓存线程池(核心线程数为0,最大线程数为Integer.MAX_VALUE的线程池),如果线程池的长度超过处理需要,可以灵活的回收空闲线程,若无可回收,则创建新线程。这种方式可能会创建大量的线程,造成OOM。
  3. newScheduledThreadPool:创建一个定长的线程池(核心线程数与最大线程数都为指定参数的线程池),支持定时及周期性任务执行。
  4. newSingleThreadExecutor:创建一个单线程化的线程池(核心线程数与最大线程数都为1的线程池),它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行
  5. newSingleScheduledThreadPool:创建一个单线程化的线程池(核心线程数为1、最大线程数为Integer.MAX_VALUE的线程池),它可安排在给定延迟后运行命令或者定期执行
  6. newWorkStealingPool:创建一个带并行级别的线程池,并行级别决定了同一时刻最多有少个;线程在执行,如不传并行级别参数,将默认为当前系统的CPU核心数*2。该线程池是在JDK1.8才出现的,底层是基于一个工作窃取算法实现的,该算法的核心思想就是让空闲的线程从其他线程的任务队列中“窃取”任务来执行。

10. Java线程池的核心参数有哪些(创建的时候)

corePoolSize:线程池的核心线程数量

maxNumPollSiza:线程池中最大的线程数

KeepAliveTime:当线程数大于核心线程数时,多余的空闲线程存活的最长时间

unit:时间单位

WorkQueue:阻塞队列/任务队列,用来储存等待执行任务的队列

threadFartoty:线程工厂,用来创建线程,一般默认即可

handler:拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务

11. 线程池的执行流程

image-20230602143058557

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
  3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用RejectedExecutionHandler.rejectedExecution()方法。

12. Synchronized底层实现原理

我们可以结合JVM和字节码来分析一下Synchronized底层实现原理

13. Synchronized与Volatile的区别

Volatile关键字可以保证变量的可见性。使用Volatile关键修饰的标量其实就是相当于指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

Synchronized关键字其实就是我们的同步锁,主要用于解决多个线程之间资源访问的同步性,被它修饰的方法或者代码块在任意时刻只能有一个线程执行。如果使用Synchronized修饰变量的话,就是将变量锁定了,当前线程可以访问该变量,其他线程被阻塞。

它们之间的区别主要有以下四点:

  • Volatile关键字是线程同步的轻量级实现,所以Volatile 性能肯定要比Synchronized 关键字要好。但是Volatile 只能作用于变量,而Synchronized 可以作用在变量、方法、类中
  • Volatile 关键字可以保证数据的可见性,但不能保证数据的原子性。Synchronized 关键字两者都能保证
  • Volatile 关键字主要用于解决变量在多个线程之间的可见性,而Synchronized 关键字解决的是多个线程之间访问资源的同步性。

Volatile除了可以保证标量的可见性之外,还有一个重要的作用就是防止 JVM 的指令重排序。

如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

面试官可能会问:“了解单例模式吗?请你用懒汉双检锁的方式给我写一个单例模式,并且解释一下”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton{
// 声明一个单例变量
private volatile static Singleton uniqueInstance;
// 构造器私有化,保证单例
private Singleton(){}
// 提供一个公开的方法,供外界获取单例实例
public Singleton getUniqueInstance(){
// 第一次检查,避免单例已经被创建的时候,其他线程都去竞争锁,从而导致阻塞
if(uniqueInstance == null){
// 加同步锁锁住类,防止高并发场景下创建多个实例
synchronized (Singleton.class){
// 第二次检查,确保实例是没有被创建的
if(uniqueInstance == null ){
// 创建实例
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

在上面的代码中,uniqueInstance 变量使用了volatile 修饰符声明,是为了防止JVM的指令重排序。比方说我们创建Singleton 实例的那一行代码uniqueInstance = new Singleton(); ,其实在JVM中是分三步完成的

  1. Singleton 类的实例对象分配内存空间。
  2. 初始化Singleton 类的实例对象
  3. uniqueInstance 变量指向Singleton 类的实例对象所在的内存地址。

正常来说我们上面的代码执行顺序就是1->2->3但是在JVM指令重排序的影响下,执行顺序可能会是1->3->2 ,这样做可以提高程序的性能,因为在执行步骤3之前,对象已经被创建,可以被其他线程访问,而步骤2中的初始化操作不影响对象的可见性和安全性。

在单线程环境下,这样做并不会产生什么问题,因为最终得到的都是一个初始化过后的实例对象嘛。

但是在并发环境下,如果线程A的执行顺序是1->3->2 ,而在3这一步的时候,线程B进来了发现uniqueInstance不为null,这个时候他就会直接返回uniqueInstance,而这个时候线程B得到的uniqueInstance是一个还未初始化的实例对象

14. ThreadLocal是什么?

ThreadLocal其实就是线程的一个局部变量。通常情况下,我们创建的变量,在多线程环境下都是共享的。如果我们想要给每个线程都创建一个属于自己的本地变量的话,我们就需要使用JDK中自带的ThreadLocal类了。**ThreadLocal类会为每一个线程创建一个独自的变量副本,这个变量副本在同一个线程内是可以传递的,在不同的线程中是隔离开的。每个线程可以通过ThreadLocal提供的get()、set()方法来获取到副本中的值。**

其实我们可以这样理解ThreadLocal:比方说有两个人去宝屋收集宝物,如果两个人都共用一个袋子的话,他们是不是有可能会发生争执,那么如果给他们各自分配一个袋子的话,是不是就避免了这个问题。我们把这两个人比作线程的话,就是他们有可能发生并发问题吧,那么ThreadLocal其实就是上面分发袋子的一个机制,用来避免(不是解决,是避免!)他们发生并发问题的。

ThreadLocal的底层实现原理:

在Thread类中,有两个ThreadLocal.ThradLocalMap 类型的属性,而ThreadLocalMap 它是ThreadLocal 下的一个静态内部类,它的结构与HashMap 类似,我们可以理解成它是ThradLocal 定制的一个HashMap 。因为它的key其实就是我们的ThreadLocal的一个弱引用,然后value就是我们要存放到线程变量副本中的值,比如我们业务中要传递的登录数据就是这个value了。

然后当我们需要获取这个value的时候,我们只需要通过调用ThreadLocal中的get(),set()方法即可,因为实际上调用这两个方法时,我们调用的是ThradLocalMap类对应的get()、set()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入到这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

总的来说ThreadLoca的实现原理就是:

每个Thread中度具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key,object为value的键值对,我们要获取或存储变量副本的值,只需要调用ThreadLocalget()/set() 方法即可

15. ThreadLocal的内存泄漏是怎么导致的?怎么避免?

首先,ThradLocal为每个线程提供变量副本是基于ThreadLocalMap 实现的,**ThreadLocalMap 中的key是ThradLocal 的一个弱引用,如果ThradLocal 没有被外部强引用的话,那么就JVM发生GC的时候,key就会被清理掉,而value不会被清理掉。**

这样一来,ThreadLocalMap 中就会出现key为null的entry,如果不加以处理的话,value是不是就永远都不会被GC给清理掉,这个时候就会造成内存泄漏了。

如何避免

ThreadLocalMap 的实现中已经考虑了内存泄漏的问题了,在调用它的get()、set()、remove() 方法的时候,会清理掉key为null的记录。当然,我们在使用完ThreadLocal之后最好还是调用一下remove()方法手动去释放一下比较好。

16. ThreadLocal的应用场景有哪些

  • 应用场景一:

    在我们的业务中,有一些业务是需要获取我们的当前用户的一些信息嘛。然后并发环境下,我们可以在做登录校验的时候,把一些用户相关的信息存放到我们的threadLocal中,那么当前线程的业务中,如果我们想要获取用户相关的信息,直接去threadLocal中取就行了。

  • 应用场景二:

    Spring事务管理的实现就有用到ThreadLocal

    首先我们Java操作数据库的一个连接技术其实就是jdbc嘛,然后jdbc在操作数据库时,是需要获取数据库连接对象connection的,而我们对数据库的事务的开启、提交、回滚操作都是可以通过connection对象来完成的。

    在Spring中,假设我们使用声明式事务来做事务管理,也就是使用@Transactional注解,我们的业务层的一个方法使用了@Transaction注解开启事务嘛,然后这个业务层方法分别调用了mapper层的两个操作数据库的方法test01,test02。

    然后在我们的事务开启前,我们是需要从数据库连接池中获取一个connection对象的,并且把我们关闭掉事务自动提交。然后这个connection对象会存放到我们的ThreadLocal中。然后再开启事务,当我们执行test01的时候,spring会先从threadlocal中获取connection对象,然后再去执行我们的数据库操作。执行test02时也是同样的一个操作。当两个方法执行完之后,spring会从threadlocal中获取connection对象,并且判断是否发生异常,如果发生了异常就使用connection.rollback回滚,否则使用connection.commit提交事务。

17. Java加锁的方式有哪些

首先在Java中是有两种类型的锁的,悲观锁和乐观锁。

悲观锁

其中,悲观锁从字面上理解就是它是很悲观的,总是假设最坏的情况;指的就是在并发环境下,无论如何,对共享资源的访问都一定会发生并发安全问题,所以在每次获取资源操作的时候都会上锁,其他线程想拿到这个资源就必须去竞争锁。也就是说,共享资源每次只能让一个线程使用,其他线程都会进入到阻塞状态,直到获得了共享资源的线程释放掉资源,其他线程才会重新去竞争锁。

悲观锁思想的常见实现有:

  • Synchronized关键字实现的同步锁
  • Lock接口的实现子类:ReentrantLock可重入锁、ReentrantReadWriteLock可重入读写锁等。

乐观锁

乐观锁从字面上理解就是很乐观,总是假设最好的情况。它认为在并发环境下,每次对共享资源的访问都不会发生并发安全问题,线程可以不停的执行,无需加锁也无需等待。只有在提交修改的时候去验证对应的资源(数据)是否被其他线程修改了即可(可以通过版本号机制或CAS算法实现)。

在Java中atomic 包下面有两个原子变量类(比如AtmoicIntegerLongAdder),就是使用了乐观锁中的一种实现方式CAS算法实现的。

乐观锁思想的常见实现有:

  • 版本号机制

    版本号机制指的就是在数据库表中加一个version字段,表示被修改的次数。当我们要执行更新操作时,就要先读取数据嘛,在读取数据时我们把version字段的值也读取,然后在提交更新的时候,只有数据库的当前版本号跟我们读取到版本号的一致时,才会更新成功,否则重试更新操作,直至更新成功

  • CAS算法,CAS算法用的比较多

    CAS算法其实就是Compare And Swap(比较与交换)

网络编程

1. 网络的7层架构

OSI七层模型是什么?每一层的作用是什么?

OSI 七层模型 是国际标准化组织提出一个网络分层模型,其大体结构以及每一层提供的功能如下图所示:

image-20230603073012982

2. TCP与UDP的区别

  1. 是否面向连接:UDP在传送数据之前不需要先建立连接。而TCP提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接
  2. 是否是可靠传输:远地主机在收到UDP报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP提供可靠的传输服务,TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制等机制。通过TCP连接传输的数据,无差错、不丢失、不重复、并且按序到达
  3. 是否有状态:与”是否可靠“相对应,TCP传输是有状态的,这个有状态说的是TCP会去记录自己发送消息的状态,比如消息是否发送了、是否被接收了等等。为此TCP需要维持复杂的连接状态表。而UDP是无状态服务,简单来说就是不管发出去之后的事情了(渣男行为)
  4. 传输效率:TCP进行传输时多了连接、确认、重传等机制,所以TCP传输效率要比UDP低很多
  5. 传输形式:TCP是面向字节流的,UDP是面向报文的
  6. 首部开销:TCP首部开销(20~60字节)比UDP首部开销(8字节)要大
  7. 是否提供广播或多播服务:TCP只支持点对点通信;UDP支持一对一、一对多、多对一、多对多通信。

总结起来就是:

条件 TCP TCP
是否面向连接
是否可靠
是否有状态
传输效率 较慢 较快
传输形式 字节流 数据报文段
首部开销 20 ~ 60 bytes 8 bytes
是否提供广播或多播服务

3. TCP与UDP的运用场景

  • UDP 一般用于即时通信,比如: 语音、 视频 、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。
  • TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。

4. TCP的三次握手和四次挥手

这个问题可以衍生出很多经典面试题,详细见下面链接

TCP 三次握手和四次挥手(传输层)

算法

算法篇详情见

JavaWeb

1. Get和Post的区别?

Get和Post都是前后端交互的请求方式,它们之前的区别有以下四点

  1. Get是不安全的,因为在传输过程,数据被放在请求的URL中;而Post的请求数据是放在请求体中的,也就是对用户来说都是不可见的,所以Post是安全的
  2. Get传送的数据量较小,一般传输数据大小在1k-18k之间(根据浏览器不同,限制不一样,但相差不大,主要是因为受URL长度限制)而Post传输的数据量较大,一般被默认为不受限制
  3. Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集
  4. Get请求效率比Post方法好。Get是form表单提交的默认方法。

2. HTTP中重定向和请求转发的区别?

它们之间的区别主要有以下四点:

  1. 重定向会发送两次请求,请求转发发送一次请求
  2. 重定向地址栏会变,请求转发地址栏不变
  3. 重定向是浏览器跳转的,请求转发是服务器跳转
  4. 重定向可以跳转到任意网址,请求转发只能跳转当前项目的

两者的实现也不同

  • 请求转发:用request的getRequestDispatcher()方法得到RequestDispatcher对象,在调用他的forward()方法实现

    1
    request.getRequestDispatcher("other.jsp").forword(request,response);
  • 重定向:调用response的sendRedirect()方法

    1
    response.sendRedirect("other.jsp")

3. Jsp和Servlet的区别

相同点

  • jsp经编译后就变成了servlet,jsp本质就是servlet,jvm只能识别java的类,不能识别jsp代码,web容器将jsp的代码编译成jvm能够识别的java类。
  • 其实就是当你通过 http 请求一个 JSP 页面时,首先 Tomcat 会将JSP翻译并编译成为 Servlet,然后执行 Servlet的生命周期方法处理请求与响应。

不同点

  • JSP侧重视图展现数据,Sevlet主要用于控制逻辑获取数据。

  • Servlet中没有内置对象 。

  • JSP中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到。

4. cookie和session的区别?

  1. 存储位置不同

    cookie的数据信息存放在客户端浏览器上。

    session的数据信息存放在服务器上。

  2. 存储容量不同

    单个cookie保存的数据<=4KB,一个站点一般保存20~50个Cookie(不同浏览器不一样,Sarafi和Chrome对每个域的Cookie数目没有严格限制)

    对于session来说并没有数据上限,但出于对服务器端性能考虑,session内不要存放过多的东西,并且设置session删除机制(或者采用缓存技术代替session)

  3. 存储方式不同

    cookie只能保存ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。

    session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。

  4. 隐私策略不同

    cookie对客户端是可见的,容易被分析本地cookie并且进行cookie欺骗,所以不安全。

    session存储在服务器上,不存在敏感信息泄露的风险。

  5. 有效期上不同

    开发者可以通过设置cookie的属性,达到是cookie长期有效的效果。

    session依赖于名为JSESSIONID的cookie,而且设置了过期时间默认为-1,只需要关闭窗口该session就会失效,因而session不能达到长期有效的效果

  6. 服务器压力不同

    cookie存储在客户端,不占用服务器资源。对于并发用户十分多的网站,cookie是很好的选择。

    session是保管在服务端的,每个用户都会产生一个session。加入并发访问的用户十分多,会产生十分多的session,耗费大量的内存。

5. 什么是Ajax,Ajax有什么优点

Ajax全称为”Asynchronous JavaScript And XML”(异步 JavaScript 和 XML),是指一种创建交互方式、快速动态网页应用的网页开发技术,无需重新加载整个网页的情况下,能够更快更新部分网页的技术。

优点:

  • 通过异步模式,可以提升用户的体验
  • 优化了浏览器和服务器之前的传输,减少了不必要的数据往返,减少了带宽占用。

5. JavaWeb的三大组件

  • Servlet:用于处理请求与响应
  • Filter:用于拦截请求与响应
  • Lisenter:用于监听三大域对象request、session、servletContext的创建与销毁,和域中数据发生变化的时候会调用监听器实现逻辑控制。

Spring

1. IOC、DI、AOP

IOC和AOP是Spring的两大核心。

  • IOC(Inversion of Control):IOC其实是一种设计思想,是指我们在创建对象的时候,将对象的控制权交给Spring容器来管理。在这之前,我们创建对象的主动权和时机都是由自己去决定的,而现在这种全力转移到Spring容器中,并由容器根据配置文件或者注解去创建实例和管理各个实例之间的依赖关系了,这样做可以在一定程度上降低对象与对象之间的耦合性,同时也提高了功能的复用(创建对象的解耦)。

    比如:在没有IOC之前,我们业务层bookService要调用dao层的实现方法bookDaoImplA时,我们只需要new一个bookDaoImplA的实例对象即可。但是在某一天我们收到一个需求,要将bookDaoImpA的实现换成bookDaoImplB了,我们就要去修改bookService中的对应的实现,牵一发而动全身,每次修改都有可能要修改大量的代码,这样的设计耦合性太高了,所以才有IOC的出现。

  • DI:DI其实就是IOC的一种实现方式,即应用程序在运行时依赖IOC容器来动态的注入外部资源(赋值或使用对象的解耦)。

  • AOP(Aspect Oriented Programming):AOP指的是面向切面编程,在开发中,会有一些与业务无关但每个业务的需要的逻辑方法,例如日志记录、事务控制、异常处理等。在AOP中,我们可以将这个逻辑代码分别抽取并封装成不同的模块,这些模块在AOP中也被称为“切面”。这样做可以减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

    Spring 的AOP是基于动态代理和cglib代理模式的,如果要代理的对象是一个接口,我们就可以通过JDK动态代理来给该对象创建一个代理对象。而如果我们要代理的对象是一个类,我们就要使用cglib代理来创建代理对象了。

2. Spring框架中用到了哪些设计模式?

工厂设计模式:Spring使用工厂模式通过BeanFactoryApplicationContext 创建Bean对象

代理设计模式:Spring Aop功能就是基于代理模式实现的,如果要代理的对象实现了某个接口,Spring 会使用JDK动态代理来创建代理对象;对于没有实现接口的对象,Spring会使用cglib代理生成一个被代理对象的子类;

因为JDK动态代理创建代理对象时,是基于实现$Proxy类来创建的

单例设计模式:Spring中的Bean默认都是单列的。Spring在底层是通过ConcurrentHashMap 实现单例注册表的特殊方式来实现单例模式的。

模板方法设计模式: 所谓模板方法设计模式就是将一些通用的代码抽取抽来,作为一个模板,然后提供一些通用的方法给外界使用。就比如说jdncTemplateRedisTemplate 等以Template结尾的类,都是用到了模板方法设计模式的。

包装器设计模式:

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

适配器模式:大家都知道SpringAOP是基于动态代理实现的,但是SpringAOP中的增强是使用到了适配器模式的;

所谓适配器模式,指的是将一个类或接口转换成我们希望的的另外一个接口,适配器模式通俗点来说就是用来做兼容的,就好比如说我们生活中大家都有可能用到的一拖三充电线其实就是适配器模式的一种体现。

而SpringAOP中,BeforeAdvice、AfterAdvice、ThrowAdvice三种类型的通知都是基于适配器模式实现的。

因为上述的几个类型的通知,

MySQL

1. MyISAM和InnoDB有什么区别?

在MySQL5.5之前,MyISAM是MySQl的默认存储引擎。虽然MyISAM的性能还行,各种特性也还不错(比如全文所以、压缩、空间函数等)。但是,由于MyISAM不支持事务以及行级锁,并且最大的缺陷就是崩溃后无法安全恢复。InnoDB正好解决了这些问题。

所以MySQL5.5之后,InnoDB是MySQL的默认存储引擎

两者的区别主要有七点

  1. 是否支持行级锁

    MyISAM只支持表级锁,而InnoDB支持表级锁以及行级锁,默认是行级锁。

    也就说,MyISAM一锁就会锁住了整张表,而InnoDB可以根据具体的场景选择锁整张表还是锁住一行,这也是InnoDB在并发写时厉害之处。

  2. 是否支持事务

    MyISAM不提供事务支持。

    InnoDB提供事务支持,实现了SQL标准定义的四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。InnoDB默认使用的是REPEATABLE-READ(可重复读)的隔离级别,而InnoDB基于MVCC(多版本并发控制)控制和Next-Key Lock解决了幻读问题

    Next-Key Lock

    Next-Key Lock是行级锁的基本单位,它是记录锁(Record Lock)和间隙锁(Gap Lock)的结合体。

  3. 是否支持外键

    MyISAM不支持外键,而InnoDB支持外键

    虽然外键对于维护数据的一致性非常有帮助,但是对性能有一定的损耗的。

    一般我们也是不建议在数据库层面使用外键的,应用层面可以解决。不过,这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。

  4. 是否支持数据库异常崩溃后的安全恢复

    MyISAM不支持,而InnoDB支持。

    使用InnoDB的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。而这个恢复的过程是依赖于redo log

  5. 是否支持MVCC

    MyISAM不支持,而InnoDB支持。

    MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。

  6. 索引的实现不一样

    虽然MyISAM引擎和InnoDB引擎都是使用B+Tree作为索引结构,但是两者的实现方式不太一样。

    InnoDB引擎中,其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的。

  7. 性能有差别。

    InnoDB的性能比MyISAM更强大,不管是在读写混合模式下还是只读模式下,随着CPU核数的增加,InnoDB的读写能力呈线性增长。MyISAM因为读写不能并发,它的处理能力跟核数没有关系。

总结:

  • InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。
  • MyISAM 不提供事务支持。InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别。
  • MyISAM 不支持外键,而 InnoDB 支持。
  • MyISAM 不支持 MVCC,而 InnoDB 支持。
  • 虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
  • MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
  • InnoDB 的性能比 MyISAM 更强大。

2. 你知道ACID吗(事务的特性)

ACID是事务的四个特性,分别是:原子性、一致性、隔离性、持久性。

  • 原子性(Atomicity):事务值最小的执行单位,不允许分割。事务的原子性确保动作要么全部被执行,要么全部不起作用。
  • 一致性(Consistency):事务实行前后,数据要保持一致。比如说在转账业务中,无论事务成功与否,转账者和收款人的总额是不变的。
  • 隔离性 (Isolation):在并发访问数据库时,会产生多个事务,在这个过程中,一个用户的事务不被其他事务所干扰,各并发事务之间的数据库时独立的。
  • 持久性(Durability):一个事务被提交之后,它对数据库中数据的改变是持久的,即使数据库产生故障也不应该对其有任何影响

只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!

隔离级别

  1. 读未提交(read Uncommited):最低的隔离界别,允许读取尚未提交的数据变更,可能会导致脏读、幻读、或不可重复度
  2. 读已提交(READ-COMMITTED):大多数数据库的默认隔离级别,该隔离级别允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复度仍有可能发生。
  3. 可重复读(REPEATABLE-READ):指的是对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复度,但幻读仍有可能发生,因为幻读是发生在DQL中的。这个隔离级别是MySQL默认的隔离级别,而MySQL在使用InnoDB数据库引擎后利用了MVCC(多版本并发控制机制)以及Next-Key lock 锁解决了幻读的问题。
  4. 可串行化(SERIALIZABLE):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。虽然该隔离级别可以防止脏读、不可重复读以及幻读,但是需要付出的代价太高了,因为他是在每个读的数据行上加锁的,可能会导致大量的超时现象和锁竞争,一般为了提升程序的吞吐量不会采用这个;

3. 脏读、幻读、不可重复读

  • 脏读(Dirty read)

    当一个事务读取数据并且对数据进行了修改时,这个修改对其他事务来说是可见的,即使当前事务还没有提交。这时,如果另外一个事务读取了这个还未提交的数据,并且第一个事务突然回滚,导致数据并没有被提交到数据库,那么第二个事务读取到的就是一个脏数据了,这也是脏读的由来

    例如:事务1读取某表中的数据A = 20, 然后事务1修改A =A-1,也就是19。在这个时候,事务2也读取这张表的数据A ,并且读取到的数据是A = 19,如果此时事务1突然回滚了,也就是A修改的数据没有被提交到数据库,即A = 20,但是事务2读取到的还是19,这就是脏读。

  • 不可重复读(Unrepeatable read)

    在一个事务内读取同一个数据时,如果在这个事务还没结束的时候,有另外一个事务也访问了这个数据,那么在第一个事务的两次读取这个数据之间,由于第二个事务对数据的修改,会导致第一个事务两次读取到的数据可能不太一样。这就发生了一个事务内连续读取到的数据不一样的情况,因此称为不可重复读。

    例如:事务1读取了某表的数据A = 20,在事务1还没结束的时候,事务2也读取了这个数据A = 20,同时对A进行了修改 A = 19,并且提交了事务2。而这时如果事务1再次读取数据A,读取到的数据会为A = 19,也就是说两次读取的数据都是不一样的。

  • 幻读(Phantom read)

    幻读其实可以看作不可重复读的一种。它发生在一个事务读取了几行数据,接着另外一个并发事务插入了一些数据时。在随后的查询中,第一个失误就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称之为幻读。

    例如:事务2想要统计某张表的一个数据,就比如说一开始的count(*) = 20嘛,然后这个时候事务1在这张表中插入了新的数据,那么这个时候事务2再次统计的count(*)=21了,也就是说他啥也不知道,就读到多一行数据了

4. 连接查询

在MySQL中,连接查询可以分为内连接和外连接。

内连接:内连接有隐式内连接和显示内连接两种

  • 隐式内连接:隐式内连接其实就是指我们连接查询时在from后面用逗号分隔两张变的连接查询
  • 显示内连接:显示内连接指的就是我们是用inner join…on进行表连接的一个操作

外连接:有左外连接和右外连接两种

  • 左外连接指的就是我们在连接查询时,以左表为基准去连接我们的另外一张表,然后他们的连接后的结果集就是左表的全部内容加上左表与右表的一个交集。连接的关键字为left join...on
  • 右外连接指的就是我们在连接查询时,以右表为基准去连接我们的另外一张表,然后他们的连接后的结果集就是右表的全部内容加上左表与右表的一个交集。连接的关键字为right join...on

5. 聚合函数

SQL中提供聚合函数有五种:

  1. COUNT:统计行数量
  2. SUM:获取某个列的合计值
  3. AVG:计算某个列的平均值
  4. MAX:计算列的最大值
  5. MIN:计算列的最小值

6. SQL关键字以及执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM 
...
join
...
on
...
where
...
group by
...avg()/sum()
having
...
SELETE
...
distinct
...
order by
...
limit
...

7. 有了解过MySQL的索引嘛?

MySQL中的索引主要有单列索引、组合索引、空间索引三种。用的比较多的是单列索引和组合索引,空间索引我这边目前还没有用过

单列索引: 在MySQL表中的某一个列上创建的索引叫做单列索引。单列索引还分为:普通索引、唯一索引、主键索引、全文索引四种。

  • 普通索引:是MySQL基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹是为了提升查询效率。
  • 唯一索引:唯一索引的索引列必须的值必须是唯一的,但是允许有控制
  • 主键索引:主键索引其实就跟我们主键的特性类似嘛,需要确保索引列中的值时唯一的且不能为null
  • 全文索引:全文索引比较特殊,只有在MyISAM引擎、InnoDB上才能使用,并且只能在CHAR、VARCHAR、TEXT类型的字段上才能使用。

组合索引: 组合索引又叫联合索引,指的是将多个字段联合起来去创建索引。

  • 组合索引的使用,需要遵循左前缀原则

  • 一般情况下,建议使用组合索引代替单列索引(主键索引除外)

    因为我们业务中的大部分查询都是涉及到多个字段的,组合索引比较于单列索引来说磁盘的IO操作和排序操作更少了,从而提高了查询效率

在设计数据库表时,如果经常需要在多个列上进行组合查询,那么使用组合索引可以显著提升查询效率。比如在订单表中,每次查询都要查询订单的状态和下单时间,那么我们就可以在这两个字段上添加一个组合索引,以提高查询效率。

8. 什么是左前缀原则

在MySQL建议联合索引时会遵循左前缀原则,即最左优先,在检索数据时从联合索引的最左边开始匹配,组合索引的第一个字段必须出现在查询语句中,这个索引才会被用到;

例如:create index index_age_name_sex on tb_user(age, name, sex) ;

上述SQL语句中,对age、name、sex 建立了一个组合索引index_age_name_sex ,实际上这个联合索引相当于(age)、(age,name)、(age, name, sex) 三个索引。