面试总结

1、数据结构中的排序算法?

直接插入排序、折半插入排序、希尔排序、冒泡排序、简单选择排序、快速排序、归并排序、堆排序。

(1)直接插入排序(稳定):将未排序序列中的元素插入到前面已排序好的元素中,最好情况时间复杂度为O(n),平均时间复杂度和最坏情况为O(n^2)。

(2)折半插入排序(稳定):设置三个变量low high mid,令mid=(low+high)/2,若a[mid]>key,则令high=mid-1,否则令low=mid+1,直到 low>high时停止循环,对序列中的每个元素做以上处理,找到合适位置将其他元素后移进行插入。他的比较次数为O(nlog2n),但是因为要后移,因此时间复杂度为O(n^2),空间复杂度为O(1)。 优点:比较次数大大减少。

(3)冒泡排序(稳定):从后往前或者从前往后两两比较相邻元素,若为逆序,则交换,直到序列比较完为止。优点是:每一趟不仅能找到一个最大的元素放到序列后面,而且还把其他元素理顺,如果下一趟排序没有发生交换则可以提前结束排序。时间复杂度为O (n^2),空间复杂度为O(1)。

(4)归并排序(稳定):把两个或者两个以上的有序表合并成一个新的有序表。时间复杂度为O(nlog2n),空间复杂度和待排序的元素个数相同。

(5)快速排序(不稳定):在序列中任意选择一个元素作为枢轴元素,比它大的元素一律向后移动,比它小的元素一律向前移动,形成左右两个子序列,再把子序列按上述操作进行调整,直到所有的子序列中都只有一个元素时序列即为有序。优点是:每一趟不仅能确定 一个元素,时间效率较高。时间复杂度为O(nlog2n),空间复杂度为O(log2n)。

(6)希尔排序(不稳定):先将序列分为若干个子序列,对各子序列进行直接插入排序,等到序列基本有序时再对整个序列进行一次直接 插入排序。优点是:让关键字值小的元素能够很快移动到前面,且序列基本有序时进行直接插入排序时间效率会提升很多,空间复杂 度为O(1),希尔排序仅适用于线性表为顺序存储的情况。

(7)简单选择排序(不稳定):每经过一趟就在无序部分找到一个最小值然后与无序部分的第一个元素交换位置。优点:实现起来特别简单,缺点:每一趟只能确定一个元素的位置,时间效率低。时间复杂度为O(n^2),空间复杂度为O(1)。

(8)堆排序(不稳定):将待排序序列构造成一个堆,输出堆顶元素,堆底元素送入堆顶,调整堆使其具有堆的性质,继续输出堆顶元素,依次往复,直到只有一个元素。优点是:对大文件效率明显提高,但对小文件效率不明显。时间复杂度为O(nlog2n),空间复杂度 为O(1)。

2、SpringBoot的注解?

(1)@SpringBootApplication注解:这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:

​ a. @SpringBootConfiguration:这个注解实际就是一个@Configuration,表示启动类也是一个配置类;

​ b. @EnableAutoConfiguration:向Spring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean;

​ c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录。

(2)@Bean注解:用来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象。

(3)@Controller、@Service、@ResponseBody、@Autowired。

SpringBoot的启动参数?

(1)命令行参数:可以在运行Spring Boot应用程序时,使用java -jar命令行并传递参数。例如:

java -jar myproject.jar –server.port=8081 –spring.datasource.url=jdbc:mysql://localhost:3306/mydb

(2)配置文件:Spring Boot 支持多种类型的配置文件,包括.properties.yml文件。application.properties是最常用的配置文件,位于src/main/resources目录下。

server.port=8081
spring.datasource.url=jdbc:mysql://localhost:3306/mydb

(3)命令行参数优先级:在启动Spring Boot应用程序时,命令行参数会覆盖配置文件中的相同参数。

例如,如果在命令行中指定了--server.port=8081,而在配置文件中定义了server.port=8080,则实际端口将是8081。

3、Spring的依赖注入?

DI:依赖注入是指在Spring IOC容器创建对象的过程中,将所依赖的对象通过配置进行注入。可以通过依赖注入的方式来降低对象间的耦合度。

Spring注入默认是单例模式,单例即对象只生成一次。若要多例的,则要添加注解@Scope(“prototype”) 。

单例注入的场景:为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。

为什么使用依赖注入

(1)开闭原则。

定义:OCP (Open Close Principle): 对扩展开放,对修改封闭。

开闭原则优点:1)易扩展。开闭原则的定义就要求对扩展开放。

2)易维护。软件开发中,对现有代码的修改是一件很有风险的事情,符合开闭原则的设计在扩展时无需修改现有代码,规避了这个风险,大大提交了可维护性。

(2)高内聚,低耦合。

高内聚是指相关度比较高的部分尽可能的集中,不要分散。

低耦合就是说两个相关的模块尽可以能把依赖的部分降低到最小,不要产生强依赖。

依赖注入的方式

在使用依赖注入时,如果注入的是Bean对象,那么要求注入的Bean对象与被注入的Bean对象都需要Spring IOC容器来实例化。

(1)通过Set方法注入

需要为注入的成员变量提供Set方法。1)POJO中添加属性、set方法和toString方法;2)Spring的xml配置文件。

(2)通过构造方法注入

Bean对象中需要提供有参的构造方法。name:根据参数名称识别参数;index:根据参数的位置来识别参数;type:根据参数的类型识别参数。

1) POJO中添加有参构造方法;2)Spring的xml配置文件。

(3)自动注入

自动注入的方式有两种,一种是全局配置自动注入,另一种是局部配置自动注入。无论全局配置或局部单独配置,都有 5 个值可以选择:no:当autowire设置为no的时候,Spring就不会进行自动注入。byName:在Spring容器中查找id与属性名相同的bean,并进行注入。需要提供set方法。byType:在Spring容器中查找类型与属性名的类型相同的bean,并进行注入。需要提供set方法。constructor:仍旧是使用byName方式,只不过注入的时候,使用构造方式进行注入。default:全局配置的default相当于no,局部的default表示使用全局配置设置。

1)局部自动注入

通过bean标签中的autowire属性配置自动注入。

有效范围:仅针对当前bean标签生效。

2)全局自动注入

通过beans标签中的default-autowire属性配置自动注入。

有效范围:配置文件中的所有bean标签都生效。

依赖注入的数据类型

(1)注入Bean对象。

(2)注入基本数据类型和字符串。

(3)注入List。

(4)注入Set。

(5)注入Map。

(6)注入Properties。

4、Spring AOP是怎么实现的?

本质是通过动态代理来实现的,主要有以下几个步骤:

(1)获取增强器,比如被Aspect注解修饰的类。

(2)在创建每一个bean时,会检查是否有增强器能应用于这个bean,就是该bean是否在该增强器指定的execution表达式中。如果是,则将增强器作为拦截器参数,使用动态代理创建bean的代理对象实例。

(3)当调用被增强过的bean时,就会走到代理类中,从而可以触发增强器,本质跟拦截器类似。

5、Websocket的通信?

与传统的HTTP请求-响应模式不同,WebSocket连接是在一个初始HTTP握手过程中建立的,然后转换为双向通信通道。这使得服务器和客户端能够在连接建立后随时互相发送数据,而不必等待请求和响应。

  1. 握手阶段
    • 客户端发起一个HTTP请求,请求头中包含了一些特定的信息,如升级协议为 WebSocket,以及一些其他的信息。
    • 服务器收到请求后,如果支持 WebSocket 协议,就会返回一个状态码为 101 的 HTTP 响应,表示握手成功。
    • 握手成功后,客户端和服务器之间的连接就会升级为 WebSocket 连接。
  2. WebSocket 连接建立
    • 一旦握手成功,客户端和服务器之间就建立了一个持久连接,可以进行实时通信。
  3. 数据传输
    • 在 WebSocket 连接建立后,客户端和服务器可以随时发送消息给对方,而不需要等待对方的请求。这使得实时通信成为可能。
  4. 关闭连接
    • 当一方希望关闭连接时,可以发送一个关闭帧,对方收到后也会发送一个关闭帧作为响应,然后双方的连接会被关闭

6、线程间通信方式?

线程通讯的方式主要可以分为三种方式,共享内存消息传递管道流。

共享内存

(1)同步–synchronized。

线程同步是线程之间按照⼀定的顺序执行,可以使用锁来实现达到线程同步,也就是在需要同步的代码块里加上关键字synchronized 。因为⼀个锁同⼀时间只能被⼀个线程持有。

(2)信号量 –volatile。

在java中,所有堆内存中的所有的数据(实例域、静态域和数组元素)存放在主内存中可以在线程之间共享,一些局部变量、方法中定义的参数存放在本地内存中不会在线程间共享。线程之间的共享变量存储在主内存中,本地内存存储了共享变量的副本。如果线程A要和线程B通信,则需要经过以下步骤:

  1. 线程A把本地内存A更新过的共享变量刷新到主内存中
  2. 线程B到内存中去读取线程A之前已更新过的共享变量。

为保证线程间的通信必须经过主内存。需要关键字volatile。

消息传递

等待/通知机制(wait/notify):一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。等待/通知机制使⽤的是使⽤同⼀个对象锁,如果你两个线程使⽤的是不同的对象锁,那它们之间是不能⽤等待/通知机制通信的。

wait()当前线程释放锁并进入等待(阻塞)状态,notify()唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后继续竞争锁notifyAll()唤醒所有正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后继续竞争锁。

管道流

管道输入/输出流用于线程之间的数据传输,传输的媒介为管道(内存)。
管道输入/输出流的体现:
基于字节流:PipedOutputStream、PipedInputStream
基于字符:PipedReader,PipedWriter

7、进程间有哪些通信方式?

进程间通信也就是指不同进程之间进行数据交换、协调和同步。

通信方式有:

(1)管道:管道是一种单向通信方式,可用于具有父子关系的进程间通信。

(2)消息队列:允许不同进程通过在队列中发送消息来进行通信。每个消息都有一个特定的类型和优先级,进程可以根据类型和优先级接收消息。

(3)信号量:用来控制多个进程对共享资源的访问。它可以用来实现进程间的同步和互斥。

(4)套接字:可用于本地进程间通信或网络进程间通信。

8、线程和进程的区别?

线程是系统分配处理器时间的最小单元。

进程指操作系统中正在运行的程序。

(1)区别:进程拥有独立的堆栈空间和数据段,需要分配独立的地址空间,开销大。

线程开销小,切换快,但没有进程安全。从通信机制上,线程可共享数据段。

(2)一个进程可以包括多个线程。

(3)不同进程间的数据很难共享。

(4)同一进程下,不同线程间数据很容易共享。

9、查看磁盘使用量的命令?

df -h (查看所有磁盘使用情况)

du -h <目录路径> (查看指定目录磁盘使用情况)

ncdu <目录路径> (以交互式方式查看磁盘使用情况)

10、Linux中查找文件的命令是什么?

(1)按文件名查找:find /path/to/search -name “filename”

(2)按文件类型查找:find /path/to/search -type f

(3)按目录名查找:find /path/to/search -type d

(4)按文件大小查找:find /path/to/search -size +10M

(5)按权限查找:find /path/to/search -perm 644

(6)按时间查找:按修改时间,find /path/to/search -mtime -7;按访问时间,find /path/to/search -atime +30;按状态改变时间,find /path/to/search -ctime 3。

11、Linux操作系统内核组成?

Linux内核主要包括进程管理、内存管理、设备驱动、文件系统、网络协议栈、系统调用。

(1)进程管理:内核负责管理系统中运行的所有进程。它分配和回收进程所需的资源,以及调度它们的执行。

(2)内存管理:内核管理系统内存的分配和释放。它负责将物理内存分配给进程,以及在需要时进行交换和页面调度等操作。

(3)文件系统:内核提供了对文件系统的支持,包括文件和目录的创建、删除、读取、写入等操作。

(4)设备驱动:内核包含了许多设备驱动程序,用于管理和控制硬件设备,如磁盘驱动、网络驱动等。

(5)系统调用:内核提供了系统调用接口,使用户空间程序可以请求内核执行特定的任务,如文件操作、进程管理等。

为什么要有文件管理?

(1)文件管理使得数据能够在磁盘等持久存储介质中进行保存,即使在计算机关闭或程序退出后也能保留下来,从而实现了数据的持久性。

(2)多个程序或用户可以共享同一个文件,这样可以在不同的应用程序之间传递和共享数据,提高了系统的灵活性和效率。

(3)文件管理允许将数据以一定的格式和结构组织起来,使得数据可以被高效地存储、检索和管理。

(4)通过文件管理,程序可以将数据以文件的形式存储,使得程序可以在不同的计算机和操作系统中运行,而不受特定硬件或操作系统的限制。

(5)文件管理系统提供了备份和恢复功能,可以定期对重要的数据进行备份,以防止数据丢失或损坏。

12、为什么说Java是编译与解释共存?

因为在程序执行的过程中,同时使用了编译和解释两种方式。在编译过程中,Java源代码(以’.java’文件扩展名保存)会被编译器(如’javac’)转换成字节码文件(以’.class’文件扩展名保存)。

然而,与其他编程语言不同,Java字节码并不是直接由硬件执行的。而Java虚拟机充当了一个“解释器”,它将字节码转换成特定平台上的本地机器码,并执行这些指令。

优点:

(1)跨平台性:Java字节码是与特定硬件无关的,可以在任何具有安装了相应JVM的平台上运行。

(2)安全性:由于Java字节码在JVM上运行,JVM可以充当一个安全屏障,阻止恶意代码对底层系统的访问。

(3)即时编译:为了提高执行速度,JVM通常还会使用即时编译器将热点代码直接编译成本地机器代码,而不是每次都解释执行。

13、Java小数为什么会有精度丢失现象,如何解决?

因为计算机内部使用二进制表示浮点数,而某些十进制小数可能无法精确地转换为二进制表示,导致了精度损失。例如,0.1在二进制中是一个无限循环的小数,因此在计算机内部以二进制形式表示时,可能会有一个近似值。

解决方法:

(1)使用BigDecimal类,提供了高精度的十进制算术运算,可以避免浮点数的精度丢失问题。

(2)由于精度问题,避免直接使用==比较浮点数,可以使用误差范围来比较。

(3)可以将小数转换成整数进行计算,然后再将结果转回小数。

14、BigDecimal的原理是什么?

将数字表示成一个整数部分和一个可选的小数部分,同时保留一个指数以标识小数点的位置。比如,123.456,可以表示成123456*10的-3次方。

15、drop/delete/truncate的区别?

(1)drop用于删除数据库对象,可以是表、视图、索引等。一旦一个对象被drop,它的所有相关数据和定义都将被删除,且无法恢复。

(2)delete用于删除表中的数据行,但保留表的结构。也就是说表的定义(列、约束等)仍然存在,只是数据被移除了。delete操作是一个事务,可以在一个事务中回滚。delete会产生日志,可能会导致性能开销。

(3)truncate用于删除表中的所有数据,但保留表的结构。相当于快速清空了表。truncate是一个DDL命令,它会直接操作表的定义,因此不能在事务中回滚。

16、static修饰变量、函数?

static修饰的变量是静态变量,属于类,而不是属于类的实例。它被所有类的实例所共享,也就是说,所有对象共用同一个静态变量。可以直接通过类名访问,无需创建对象。静态变量会在类第一次加载时被初始化,且只会初始化一次。

static修饰的方法是静态方法,属于类,而不属于类的实例。它不能访问类的实例变量或方法,只能访问静态变量和调用静态方法。

静态方法可以直接通过类名调用,无需创建对象。静态方法中不能使用this关键字,因为它没有当前对象的引用。

静态变量适合用于表示所有实例共享的数据,比如类的计数器、常量等。

静态方法通常用于工具类或者一些独立于实例的操作,例如数学计算工具类、工厂方法等。

17、高级语言、汇编语言、编译语言、机器语言的区别?

(1)机器语言是计算机可以直接理解和执行的语言,它是由二进制代码组成的,代表特定的机器指令和数据。比如,x86架构的机器语言可以是一系列由0和1组成的二进制命令,每条指令都对应着一个特定的操作。

(2)汇编语言是一种符号化的低级语言,它使用英文单词(助记符)来代表机器语言中的指令和数据。每个助记符通常对应着一条机器语言指令。例如,x86架构的汇编语言中,MOV指令用于数据移动,ADD指令用于加法操作,它们是对应机器语言中具体指令的符号表示。

(3)高级语言是相对于机器语言和汇编语言而言的更抽象、更易读写的语言,它以自然语言(如英语)为基础,使用类似于人类语言的语法和结构。例如,C、C++、Java、Python等都是高级语言。

(4)编译语言是指使用编译器将高级语言代码转换为机器语言的语言。编译器将整个程序翻译成机器语言,生成一个可执行文件,用户可以在计算机上直接运行。例如,C、C++等都是编译语言,程序员编写源代码后,需要先将其编译成可执行文件,然后才能在计算机上运行。

18、动态链接库和静态链接库的区别?

(1)动态链接库和静态链接库都是以文件形式存在,动态链接库通常以“.dll”(Windows)或“.so”(Unix/Linux)为扩展名。静态链接库通常以”.lib”(Windows)或”.a”(Unix/Linux)为扩展名。

(2)动态链接库,在程序运行时,它的代码会被加载到内存中,多个程序可以共享同一个动态链接库的实例。而静态链接库,在编译时,静态链接库的代码会被完全复制到可执行文件中,形成一个独立的可执行文件。

(3)动态链接库的程序大小不会使得可执行文件变大,因为库代码不会被复制到可执行文件中。静态链接库会使可执行文件变大,因为所有库代码都会被复制到可执行文件中。

(4)动态链接库中程序依赖于系统中已安装的动态链接库,因此需要确保目标系统上有相应的库。静态链接库不需要额外的运行时支持,因为所有的代码已经被链接到可执行文件中。

19、线程同步用到的锁?这些锁的使用场景?

对象锁(内部锁和监视器锁)和显示锁(Lock接口的实现类)。

(1)对象锁,通过在方法前面加上synchronized关键字,或者在代码块中使用synchronized(obj)的形式来实现。对象锁的作用范围是整个方法或代码块,当一个线程获得了对象锁时,其他线程就必须等待,直到该线程释放锁。

适用场景:当多个线程需要访问共享资源时,可以使用对象锁来保证同一时刻只有一个线程可以访问。对象锁适用于简单的同步需求,通常用于对某个对象的状态进行读写保护。

(2)显示锁(Lock接口的实现类):显示锁是通过Lock接口及其实现类(如ReentrantLock)来实现的。需要手动调用lock()和unlock()来获取和释放锁。显示锁的作用范围由程序员手动控制,可以根据需要来精确控制锁的范围。

适用场景:显示锁适用于需要更灵活地控制锁的获取和释放的场景,提供了一些额外的功能,如尝试非阻塞地获取锁、定时地获取锁等。

20、为什么需要JVM虚拟机?

(1)具有跨平台性:Java程序在编译时被翻译成中间字节码,而不是特定于某个硬件平台的机器码。JVM在不同的操作系统上实现了对这些字节码的解释或者编译成本地机器码的功能,使得Java程序可以在不同的平台上运行。

(2)垃圾回收:JVM提供了垃圾回收机制,自动管理程序中不再使用的内存,减轻了程序员对内存管理的负担,防止了内存泄露。

(3)内存管理:JVM会自动管理程序的内存分配和释放,包括堆和栈的内存管理,使得程序员无需关心底层的内存分配细节。

(4)异常处理:JVM提供了强大的异常处理机制,可以帮助程序员识别和处理各种异常情况,保证程序的稳定性。

21、如果没有JVM虚拟机会怎么样?

(1)Java的跨平台特性将会丧失,因为没有JVM来解释或编译字节码,Java程序将无法在不同的操作系统上运行。

(2)如果没有JVM,程序员将不得不手动管理内存分配和释放,容易导致内存泄漏和内存溢出等问题。

(3)JVM负责垃圾回收,自动清理不再使用的内存。如果没有JVM,程序员将不得不手动管理内存,容易导致内存泄漏。

(4)JVM内建了对多线程的支持,如果没有JVM,将无法方便地创建和管理线程,实现并发编程将变得困难。

22、最长回文子串(定义回文串长度为偶数,奇数不看作回文串)?

可以使用动态规划的方法。

通常使用一个二维数组dp来表示状态,其中 dp[i][j] 表示从字符串的第 i 个字符到第 j 个字符是否为回文子串。

初始化:对角线上的元素都是 true,因为单个字符本身也是一个回文串。

状态转移方程:

  • 如果 s[i] == s[j] 并且 dp[i+1][j-1]true,那么 dp[i][j] = true,因为两个相同的字符会在已知的回文子串的两侧增加长度为2的回文子串。
  • 如果 s[i] != s[j],那么 dp[i][j] = false

填表:从右下角开始,按照状态转移方程填表,最终找到 dp[i][j]true 的最大子串。

回溯:如果需要找到具体的最长回文子串,可以根据 dp 数组反推得到。

也可以使用双指针。

(1)遍历每个可能的回文中心:由于回文串有奇数和偶数两种情况,对于每个位置 i,都可以将其视为回文中心,然后使用两个指针从该中心向两侧扩展,直到不再是回文串为止。

(2)判断回文串:判断一个字符串是否是回文串可以使用双指针法,即从两侧同时向中间移动,判断对应位置的字符是否相等。

(3)记录最长回文子串:在遍历过程中,记录下最长的回文子串的起始位置和长度。

23、红黑树与哈希表的区别?

红黑树是一颗自平衡的二叉搜索树,

  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色的。
  • 叶子节点(NIL节点)是黑色的。
  • 如果一个节点是红色的,则它的两个子节点都是黑色的。
  • 从任意节点到其每个叶子的所有路径都包含相同数目的黑色节点。

哈希表是一种以键值对存储数据的数据结构,通过计算哈希函数将键映射到表中的一个位置(桶)来实现快速的数据访问。

红黑树的插入和删除操作可能需要进行树的旋转和重新着色,以保持树的平衡性。查找操作的时间复杂度为O(log n),因为红黑树是一棵二叉搜索树。并且红黑树是一种有序的数据结构,它保持了元素的有序性。

哈希表查找操作的平均时间复杂度是O(1),在理想情况下,可以实现常数级别的时间复杂度。哈希表中的元素是无序的,它们的存储位置是根据哈希函数计算得到的。哈希表可能会发生哈希冲突,即不同的键映射到了同一个位置。解决冲突的方法包括链地址法和开放地址法等。

24、IO多路复用的理解?

I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

25、epoll和select?

epoll和select都是用于处理I/O多路复用的系统调用。

(1)select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了IO事件。

(2)poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了IO事件。

(3)epoll模型,epoll和poll是完全不同的,epoll是一种事件通知模型,当发生了IO事件时,应用程序才进行IO操作,不需要像poll模型那样主动去轮询。

26、Socket的理解与调用?

Socket(套接字)是在网络通信中使用的一种机制,它提供了一种通用的数据传输方式,使得不同设备之间可以进行数据交换。是基于客户端-服务器模型的。一个程序(客户端)通过一个Socket连接到另一个程序(服务器)。通信的两端都需要有唯一的标识,通常使用IP地址和端口号来标识。IP地址指明网络中的主机,端口号用于区分同一主机上的不同服务。Socket通信使用TCP/IP协议,TCP(传输控制协议)提供可靠的、面向连接的通信,而UDP(用户数据报协议)提供不可靠的、面向无连接的通信。

基本的Socket调用流程:

服务器端

  1. 创建一个ServerSocket对象,指定端口号。
  2. 调用accept()方法监听客户端的连接请求,当有客户端连接时,返回一个Socket对象用于和该客户端通信。
  3. 使用Socket的输入输出流与客户端进行通信。
  4. 关闭Socket和ServerSocket。

客户端

  1. 创建一个Socket对象,指定要连接的服务器的IP地址和端口号。
  2. 使用Socket的输入输出流与服务器进行通信。
  3. 关闭Socket。

27、浮点数的存储,小数部分靠近0还是靠近1表示更精确?

靠近1更精确。

浮点数通常使用IEEE 754标准来表示,它将一个浮点数分成三部分:

  1. 符号位:用来表示正负号,1位。
  2. 指数部分:用来表示数的次方,通常用偏移码表示,占用固定的位数。
  3. 尾数部分:也称为尾数或者尾数部分,用来表示有效数字,占用固定的位数。

在二进制表示中,尾数部分的每一位都代表了一个负幂次的二进制分数,例如:第一位表示2的-1次方,第二位表示2的-2次方,以此类推。

因此,尾数部分的最后一位(即最小的2的负幂次)表示的是一个最小单位。这意味着,如果尾数部分的某一位靠近1的地方发生了变化,它对浮点数的影响会比靠近0的地方更为显著,因为它代表了一个较大的数值变化。

28、查找数组中第k大的元素?

使用一个最大堆来维护前k个最大的元素,遍历数组,将元素依次插入堆中,如果堆的大小超过k,则将堆顶元素(最大元素)弹出。最终堆顶元素就是第k大的元素。

29、两个栈实现一个队列?

(1)定义两个栈,分别称为stack1stack2

(2)入队操作时,将元素压入stack1

(3)出队操作时,先检查stack2是否为空,如果不为空,则直接从stack2弹出元素;如果为空,将stack1中的所有元素依次弹出并压入stack2,然后再从stack2中弹出元素,保证了队列的先进先出顺序。

30、两个队列实现一个栈?

(1)定义两个队列,分别称为queue1queue2

(2)入栈操作时,将元素放入非空的队列(假设初始时queue1非空)。

(3)出栈操作时,将非空队列中的前n-1个元素依次出队并入队到另一个空队列中,然后将最后一个元素出队,即为出栈的元素。

数据结构

数据结构

1、顺序表和链表的比较

①存取(读取)方式

顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。

②逻辑结构与物理结构

采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来实现的。

③查找、插入和删除操作

查找:对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O (log2n)。对于按序号查找,顺序表支持随机访问,时间复杂度仅为O(1),而链表的平均时间复杂度为O(n)。

插入、删除:顺序表的插入、删除操作,平均需要移动半个表长的元素;链表的插入、删除操作,只需要修改相应的结点指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大。

④空间分配

顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新的元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。动态分配存储虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。

链式存储的结点空间只在需要时申请分配,只要内存有空间就可以连续分配,操作灵活、高效。

2、解释下头指针和头结点的区别?单链表增加头结点的目的是什么?

头指针:是指向第一个结点存储位置的指针,具有标识作用,无论链表是否为空,头指针都存在。

头结点:是为了操作统一和方便设立的,放在第一个元素结点之前,头结点的数据域可以不存储任何信息,因此头结点可有可无。

3、数组和链表的区别是什么?

从逻辑结构来看:数组的存储长度是固定的,即数组大小定义之后不能改变,而且在插入和删除操作需要移动大量元素,相反对于链表,它能够动态分配存储空间以适应数据动态增减的情况,并且易于进行插入和删除操作。

从访问方式来看:数组中的元素在内存中连续存放,可以通过数组下标进行随机访问,访问效率比较高。链表是链式存储结构,它的存储空间不是必须连续,可以是任意的,因此访问链表必须从前往后依次进行,访问效率比较低。

4、栈的应用有哪些?

括号匹配、表达式的计算、递归的应用。

括号匹配:遇到左括号就将其压入栈中,遇到右括号就将栈顶的左括号弹出,检查其是否与当前扫描的右括号匹配。

表达式的计算:前缀、后缀表达式,运算符在两个操作数前面、后面。从左往右依次扫描下一个元素,直到处理完所有元素;若扫描到操作数就将其压入栈中,并继续扫描,若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,并继续扫描。

递归的应用:每进一层递归,就将递归调用所需要的信息压入栈顶;每退出一层递归,就从栈顶弹出相应信息。

5、介绍一下字符串模式匹配算法:朴素模式匹配算法的KMP算法?

串的模式匹配:在主串中找到与模式串相同的字串,并返回其所在位置。

朴素模式匹配算法:将模式串的字符与主串中的每一个字串一一进行对比。

KMP:指向主串的指针不回溯,依次往后进行匹配比较,而指向模式串的指针需要回溯,当某个位置的字符匹配失败时,模式串指针 根据next数组指向相应的位置,在进行匹配。

6、如何构造哈夫曼树?哈夫曼编码的意义是什么?

构造哈夫曼树的算法描述如下:

1)将n个结点分别看作n棵仅含一个结点的二叉树。

2)在所有的根节点中选取两个根节点权值最小的数构造成新的二叉树,再将根节点与其他n-1个根节点进行比较,选出根节点权值最小的两棵树进行归并,重复上述步骤,直至所有结点都归并到一个树上为止。

哈夫曼编码的意义:

将出现频率高的字符使用较短的编码,反之出现频率较低的则使用较长的编码,降低字符串的平均期望长度。频繁使用的机器指令操作使用较短的编码,这样会提高执行的效率。

7、什么是二叉树?什么是二叉排序树?

二叉树:是一种树形结构,其特点是每个结点至多只有两颗子树,并且二叉树的子树有左右之分,其次序不能任意颠倒。

二叉排序树:是左子树结点值<根节点值<右子树结点值。

平衡二叉树:树上任意结点的左子树和右子树的深度差不超过1.

满二叉树:一颗二叉树的结点要么是叶子结点要么它有两个子节点。

完全二叉树:若设二叉树的深度为h,除第h层外,其他各层节点数都达到最大个数,第h层结点都连续集中在最左边。

最优二叉树:也叫哈夫曼树,树的带权路径长度达到最小,权值较大的结点离根较近。哈夫曼树的特点:没有度数为1的结点,又 称为正则二叉树。n 个叶子节点的哈夫曼树,度数为2的节点数为n-1个,所以哈夫曼树一共有2n-1个节点。

8、为什么要引入平衡二叉树?

平衡二叉树,一定是二叉排序树,之所以将其排序调整为平衡状态,是为了在二叉排序树近似为链的情况下增强其查找性能,降低时间复杂度。

9、简述一下广度优先遍历和深度优先遍历?

广度优先遍历:首先访问结点v,由近至远依次访问和v邻接的未被访问过的结点,类似于层次遍历。

深度优先遍历:首先访问顶点v,若v的第一个邻接点没有被访问过,则访问该邻接点;若v的第一个邻接点已经被访问,则访问其第二个邻接点;类似于先序遍历。

10、简述一下求最小生成树的两种算法?

最小生成树:生成树集合中,边的权值之和最小的树。

普利姆(prim)算法:从某一个顶点开始构建生成树,每次将代价最小的新的顶点纳入生成树中,直到所有顶点都纳入为止,适用于 边稠密图。

克鲁斯卡尔(kruskal)算法:按照边权值递增的次序构建生成树,每次选择一条权值最小的边,使这条边的两头连通(原本已连通 就不选),直到所有结点都连通为止,适用于边稀疏图。

11、简述一下求最短路径的两种算法?

最短路径:把带权路径长度最短的那条路径称为最短路径。

迪杰斯特拉(Dijkstra)算法:求单源最短路径,用于求某一顶点到其他顶点的最短路径,它的特点是以起点为中心层层向外扩展, 直到扩展到终点为止,迪杰斯特拉算法要求的边权值不能为负。

弗洛伊德(Floyd)算法:求各顶点之间的最短路径,用于解决任意两点间的最短路径,它的特点是可以正确处理有向图或负权值的 最短路径问题。

12、简述一下什么是拓扑排序?

拓扑排序:它是一个有向边无环图,它的思想是选择一个入度为0的顶点并输出,然后删除此顶点以及所有出度,循环直到结束。

13、简述下邻接矩阵与邻接表的区别?

邻接矩阵:矩阵的第i行第j列表示i到j是否连接。

邻接表:链表后面跟着所有指向的点。

邻接矩阵的优点是可以很方便的知道两个节点之间是否存在边,以及快速的添加或删除边;缺点是如果节点个数比较少容易造成存储 空间的浪费。

邻接表的优点是节省空间,只给实际存在的边分配存储空间;缺点是在涉及度时可能需要遍历整个链表。

14、谈谈你对散列表的认识?

散列表又称哈希表,是一种数据结构,特点是数据元素的关键字与其存储地址直接相关。常用的散列函数的构造方法有:直接定址 法、除留余数法、数字分析法、平方取中法。常见的处理冲突的方法有:开放定址法(线性探测法、平方取中法、再散列法、伪随机 序列法)、拉链法。散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子。

15、数据结构有哪些查找算法?它们分别适用于什么样的存储结构?

顺序查找:又称线性查找,它对顺序表和链表都是适用的,对于顺序表,可通过数组下标递增的顺序来扫描每个元素;对于链表,可 通过next来依次扫描每个元素。

折半查找:又称二分查找,它仅适用于有序的顺序表。

分块查找:又称索引顺序查找,它将查找表分为了若干子块,块内元素可以无序,但块间元素是有序的。

16、循环比递归效率高?

递归和循环两者完全可以互换。不能完全决定性地说循环地效率比递归的效率高。

①递归

优点:代码简洁、清晰,并且容易验证正确性。

缺点:效率较低,递归是有时间和空间消耗的,递归中很多计算都是重复的,从而给性能带来很大的负面影响。

②循环

优点:速度快,结构简单。

缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环。

17、概述一下大根堆的构造过程?并说说堆排序的适用场景。

对于第[n/2]个结点为根的子树进行筛选,使得它的根结点大于等于左右子树(小根堆反之),之后向前依次([n/2]-1~1)为根的子树进行筛选,看该结点值是否大于其左右子树结点,若不大于,则将左右子结点中较大者与之交换,交换后可能会破坏下一级堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构建成堆为止。

堆排序适用于关键字较多的情况,比如在1亿个数中选出前100个最大值。

Redis

Redis

0、Redis和其他数据库的区别?

Redis不使用表,数据库也不会预定义或者强制去要求用户对Redis存储的不同数据进行关联。

(1)和高性能键值缓存服务器memcached对比:

Redis和memcached都可用于存储键值映射,但是Redis能够自动以两种不同的方式将数据写入硬盘进行持久化;Redis除了能存储普通的字符串键外,还可以存储其他四种数据结构,而memcached只能存储普通的字符串键;Redis数据库既可以用作主数据库使用,也可以作为其他存储系统的辅助数据库使用。

(2)和mysql的对比:

MySQL是关系型数据库,主要用于存放持久化数据,将数据存储在硬盘中,读取速度较慢。

redis是非关系型数据库,也是缓存数据库,将数据存储在缓存中,缓存的读取速度快,能够大大的提高运行效率,但是保存时间有限。

1、Redis有哪些数据结构?分别有哪些典型的应用场景?

(1)字符串:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID。

(2)哈希表:可以用来存储一些key-value对,更适合用来存储对象。

(3)列表:通过命令的组合,既可以当作栈,也可以当作队列来使用,可以用来缓存类似微信公众号、微博等消息流数据。

(4)集合(String字符串类型的无序集合):和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能。

(5)有序集合(Zset是String类型的有序集合):不可重复,有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。

2、Redis和Mysql如何保证数据一致?

延迟双删。先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,就算再更新Mysql时,有其他线程读了Mysql,把老数据读到了Redis中,那么也会被删除掉,从而使数据保持一致。

(1)先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然会导致不一致。

(2)先删除Redis缓存数据,再更新Mysql,再次查询的时候再将数据添加到缓存中,这种方案能解决方案1的问题。但是在高并发下性能较低,而且仍然会出现数据不一致的问题,比如线程1删除了Redis缓存数据,正在更新Mysql,此时另一个线程在查询,那么就会把Mysql中老数据又查到Redis中。

3、Redis如何保证数据的持久化?

Redis提供两种持久化机制:RDB(默认) 和AOF机制,Redis4.0以后采用混合持久化,用AOF来保证数据不丢失,作为数据恢复的第一选择;用RDB来做不同程度的冷备。

(1)RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

优点:1)只有一个文件dump.rdb,方便持久化;2)容灾性好,一个文件可以保存到安全的磁盘。 3)性能最大化,fork子进程来进行持久化写操作,让主进程继续处理命令,只存在毫秒级不响应请求。 4)数据集大时,比AOF的启动效率更高。

缺点: 数据安全性低,RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。

(2)AOF持久化,是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis,会重新将持久化的日志中文件恢复数据。

优点: 1)数据安全,AOF持久化可以配置appendfsync属性,有always,每进行一次命令操作就记录到AOF文件中一次。 2)通过append模式写文件,即使中途服务器宕机,可以通过redis-check-aof工具解决数据一致性问题。

缺点:1)AOF文件比RDB文件大,且恢复速度慢。 2)数据集大的时候,比RDB启动效率低。

4、Redis是单线程还是多线程?

对于读写命令来说,Redis一直是单线程模型。但是在Redis 4.0版本之后引入了多线程来执行一些大键值对的异步删除操作,Redis 6.0版本之后引入了多线程来处理网络请求,提高网络IO读写性能。

Redis基于Reactor模式设计开发了一套高效的事件处理模型,这套事件处理模型对应的是Redis中的文件事件处理器。因为文件事件处理器是单线程方式运行的,所以一般都说Redis是单线程模型。

(1)文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

(2)当被监听的套接字准备好执行连接应答、读取、写入、关闭等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

文件事件处理器包括四部分:多个socket(客户端连接);IO多路复用程序(支持多个客户端连接的关键);文件事件分派器(将socket关联到相应的事件处理器);事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。

5、Redis是单线程的,但是为什么还那么快?

1、完全基于内存的,C语言编写。

2、采用单线程,避免不必要的上下文切换可竞争条件。

3、Redis通过I/O多路复用程序来监听来自客户端的大量连接,它会将感兴趣的事件及类型注册到内核中并监听每个事件是否发生。I/O多路复用技术的使用让Redis不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。

6、Redis哨兵?

Redis哨兵模式是一种用于保证Redis集群高可用性的架构方案。

哨兵模式由多个哨兵进程组成,负责监控集群中的主节点和从节点,并在发生故障时触发故障转移流程。通过哨兵模式,可以确保Redis集群在发生故障时能够自动恢复,从而保证集群的高可用性。并且Redis哨兵模式还具有监控集群状态的功能,可以通过哨兵进程实时监测集群中各个节点的状态,并发出警报或触发自动恢复流程。

工作原理:主哨兵负责监控集群中的主节点,如果发现主节点出现故障,则会触发故障转移流程。备用哨兵则负责监控集群中的从节点,如果发现从节点出现故障,则会向主哨兵发出警报。当主哨兵接收到警报或发现主节点出现故障时,它会开始选举新的主节点。选举过程中,哨兵会按照一定的规则确定新的主节点。例如选取从节点作为主节点,或者选取集群中存活时间最长的节点作为主节点等。在选举完成后,哨兵会将新的主节点的信息广播给集群中的所有节点,并将从节点转换为新的从节点,进行数据同步。此时,集群已经完成了故障转移,并且恢复了正常的工作状态。

优点:

(1)高可用性:Redis哨兵模式能够在发生故障时自动触发故障转移流程,从而保证集群的高可用性。

(2)简单易用:Redis哨兵模式的配置和使用都非常简单。

(3)扩展性强:Redis哨兵模式能够支持多个主节点,因此在扩展集群规模时也很方便。

缺点:

(1)单点故障风险:Redis哨兵模式中的哨兵进程是单点的,如果哨兵进程出现故障,则可能导致整个集群无法工作。因此,建议至少配置3个哨兵进程,以保证哨兵的高可用性。

(2)选举过程耗时:当主节点出现故障时,哨兵会开始选举新的主节点,这个过程可能会消耗一定的时间。因此,如果对故障恢复的时间有特别的要求,可能需要采用其他的高可用方案。

(3)监控成本较高:Redis哨兵模式中的哨兵进程需要与集群中的所有节点进行连接,并定期监测集群状态,这可能会带来较高的监控成本。

7、缓存穿透?

缓存穿透是指大量请求的key是不合理的,根本不存在于缓存中,也不存在于数据库中。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能会宕机。

解决方案—:缓存无效key。如果缓存和数据库都查不到某个key的数据,就写到Redis中并设置过期时间。这可以解决请求的key变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求key,会导致Redis中缓存大量无效的key。如果用这种方式来解决穿透问题的话,尽量将无效的key的过期时间设置短一点比如1分钟。

解决方案二:添加布隆过滤器。

把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

优点:内存占用较少,没有多余的key;缺点:实现复杂,存在误判,一般设置为百分之5。

当一个元素加入布隆过滤器中的时候,会进行什么操作?

(1)使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值。

(2)根据得到的哈希值,在位数组中把对应下标的值置为1。

当判断一个元素是否存在于布隆过滤器的时候,会进行什么操作?

(1)对给定元素再次进行相同的哈希计算。

(2)得到值之后判断位数组中的每个元素是否都为1,如果值都为1,说明这个值在布隆过滤器中,如果存在一个值不为1,说明该元素不在布隆过滤器中。

一定会出现这种情况:不同的字符串可能哈希出来的位置相同。(可以适当增加位数组大小或者调整哈希函数来降低概率)

8、缓存击穿?

缓存击穿指请求的key对应的是热点数据,该数据存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。这可能会导致瞬时大量的请求直接到达数据库上,对数据库造成了巨大的压力,可能会宕机。

解决方法:

(1)设置热点数据永不过期或者过期时间比较长。

(2)针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。

(3)可以使用互斥锁。当缓存失效时,不立即去load db,先使用如Redis的setnx去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法。

9、缓存穿透和缓存击穿有什么区别?

缓存穿透中,请求的key既不存在缓存中,也不存在于数据库中。

缓存击穿中,请求的key对应的是热点数据,该数据存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。

10、缓存雪崩?

缓存雪崩指缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。

另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。

解决方案:

针对Redis服务不可用的情况。

(1)采用Redis集群,避免单机出现问题,整个缓存服务都没办法使用。

(2)限流,避免同时处理大量的请求。

针对热点缓存失效的情况。

(1)设置不同的失效时间,比如随机设置缓存的失效时间。

(2)缓存用不失效。

(3)设置二级缓存。

11、缓存雪崩和缓存击穿有什么区别?

缓存雪崩导致的原因是缓存中的大量或者所有数据失效。

缓存击穿导致的原因主要是某个热点数据在缓存中不存在。

12、Redis集群有哪些方案?

主从复制、哨兵模式、Redis分片集群。

单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。

哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用。

分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。

13、Redis集群脑裂?

由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。

14、Redis的失效策略?

缓存失效策略 :

(1)定时清除:针对每个设置过期时间的key都创建指定定时器。

(2)惰性清除:访问时判断,对内存不友好。

(3)定时扫描清除:定时100ms随机20个检查过期的字典,若存在25%以上则继续循环删除。

15、Redis的淘汰策略?

(1)全局的键空间选择性移除。

noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(字典库常用)

allkeys-lru:在键空间中,移除最近最少使用的key。(缓存常用)

allkeys-random:在键空间中,随机移除某个key。

(2)设置过期时间的键空间选择性移除 。

volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。

volatile-random:在设置了过期时间的键空间中,随机移除某个key。

volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。

16、Redis事务?

Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会 插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。 Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。

17、Redis底层结构?

SDS数组结构,用于存储字符串和整型数据及输入缓冲。

跳跃表、字典、渐进式rehash、压缩列表zipList、整数集合intSet、快速列表quickList。

18、Zset的底层实现?

跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。 简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度。

Zset数据量少的时候使用压缩链表ziplist实现,有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存 member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。 数据量大 的时候使用跳跃列表skiplist和哈希表hash_map结合实现,查找删除插入的时间复杂度都是O(longN) 。Redis使用跳表而不使用红黑树,是因为跳表的索引结构序列化和反序列化更加快速,方便持久化。

(1)搜索

跳跃表按 score 从小到大保存所有集合元素,查找时间复杂度为平均 O(logN),最坏 O(N) 。

(2)插入

选用链表作为底层结构支持,为了高效地动态增删。因为跳表底层的单链表是有序的,为了维护这种有序性,在插 入前需要遍历链表,找到该插入的位置,单链表遍历查找的时间复杂度是O(n),同理可得,跳表的遍历也是需要遍 历索引数,所以是O(logn)。

(3)删除

如果该节点还在索引中,删除时不仅要删除单链表中的节点,还要删除索引中的节点;单链表在知道删除的节点是 谁时,时间复杂度为O(1),但针对单链表来说,删除时都需要拿到前驱节点O(logN)才可改变引用关系从而删除目 标节点。

19、Redis的读写模式?

(1)CacheAside旁路缓存

写请求更新数据库后删除缓存数据。读请求不命中查询数据库,查询完成写入缓存。

(2)Read/Write Though(读写穿透)

场景: 微博Feed的Outbox Vector(即用户最新微博列表)就采用这种模式。一些粉丝较少且不活跃的用户发表微博后,Vector服务会首先查询Vector Cache,如果cache中没有该用户的Outbox记录,则不写该用户的cache数据,直接更新DB后就返回,只有cache中存在才会通过CAS指令进行更新。

(3)Write Behind Caching(异步缓存写入)

比如对一些计数业务,一条Feed被点赞1万次,如果更新1万次DB代价很大,而合并成一次请求直接加1万,则是 一个非常轻量的操作。但这种模型有个显著的缺点,即数据的一致性变差,甚至在一些极端场景下可能会丢失数据。

20、多级缓存?

浏览器本地内存缓存:专题活动,一旦上线,在活动期间是不会随意变更的。

浏览器本地磁盘缓存:Logo缓存,大图片懒加载。

服务端本地内存缓存:由于没有持久化,重启时必定会被穿透 。

服务端网络内存缓存:Redis等,针对穿透的情况下可以继续分层,必须保证数据库不被压垮。

为什么不使用服务器本地磁盘做缓存?

当系统处理大量磁盘IO操作的时候,由于CPU和内存的速度远高于磁盘,可能导致CPU耗费太多时间等待磁盘返 回处理的结果。对于这部分CPU在IO上的开销,我们称为iowai。

21、String的底层实现是什么?

String自己编写了SDS(简单动态字符串)来作为底层实现。

SDS有五种实现方式SDS_TYPE_5(并未用到)、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64,其中只有后4种实际用到。Redis会根据初始化的长度决定使用哪种类型,从而减少内存的使用。

后四种实现都包含4种属性:

(1)len:字符串的长度也就是已经使用的字节数。

(2)alloc:总共可用的字符空间大小,alloc-len就是SDS剩余的空间大小。

(3)buf[]:实际存储字符串的数组。

(4)flags:低三位保存类型标志。

SDS相比于C语言种的字符串有如下提升:

(1)可以避免缓冲区溢出:C语言中的字符串被修改时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS被修改时,会先根据len属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。

(2)获取字符串长度的复杂度较低:C语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为O(n)。SDS的长度获取直接读取len属性即可,时间复杂度为O(1)。

(3)减少内存分配次数:为了避免修改字符串时,每次都需要重新分配内存,SDS实现了空间预分配和惰性空间释放两种优化策略。当SDS需要增加字符串时,Redis会为SDS分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当SDS需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用。

(4)二进制安全:C语言中的字符串以空字符\0作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C字符串无法正确保存。SDS使用len属性判断字符串是否结束,不存在这个问题。

22、如果大量数据更新到DB的时候失败了怎么办?

(1)事务回滚:事务回滚会将数据库恢复到更新之前的状态,以避免脏数据的存在。

(2)分批次更新:将数据分成较小的批次进行更新,而不是一次性更新大量数据。

(3)优化查询和索引:确保数据库表的查询和索引设计得合理。可以提高数据更新的效率,减少可能发生失败的机会。

(4)逐个处理失败的数据:可以考虑逐个处理失败的数据,而不是一次性处理大量数据。

23、给别人转账时,要先扣钱再加钱,这两步放在不同服务器上,如何保持数据一致性?

(1)分布式锁:在第一步服务器扣钱时,使用一个全局的分布式锁来确保在扣钱和加钱之间没有其他操作。这可以保证两步操作之间的原子性。

(2)分布式事务:使用分布式事务管理器(2PC、3PC)来保证两个操作要么同时成功,要么同时失败。

(3)消息队列/事件驱动架构:在第一步扣钱成功后,将转账请求发送到一个消息队列中。第二步服务器订阅此队列,并处理转账请求。

(4)幂等性处理:多次执行相同的操作对系统的状态不会产生变化。所以就算是因为某种原因导致了重复的操作,也不会影响最终的一致性。

24、Redis在做缓存存储的时候是以DB为主参考还是以redis缓存为主参考?

以Redis缓存为主参考。当Redis用作缓存时,通常是将数据从数据库中读取出来,然后缓存在Redis中,以加速对该数据的访问。Redis是作为一个缓存层,它可以在访问频繁的数据和数据库之间提供一个高速的中间存储。

Redis并不是单纯依赖于数据库中的数据来提供缓存,而是根据业务需求来决定什么时候将数据存储在Redis中,以及何时从Redis中读取数据。

Redis的作用是提供一个高速的中间存储,以减少对数据库的访问压力,提升系统性能。

25、使用Redis作为缓存时的一般流程?

(1)读取操作:当客户端需要某个数据时,首先会尝试从Redis缓存中获取数据。如果Redis中存在该数据,就直接返回给客户端,无需访问数据库。

(2)缓存失效:如果Redis中没有该数据,或者缓存中的数据已经过期,客户端会尝试从数据库中读取数据。

(3)数据写入Redis:从数据库中读取到的数据会被存储在Redis中,以便下次访问时可以直接从Redis中获取。

(4)定期刷新或失效:缓存中的数据可能会因为一些原因失效(例如设定了过期时间)。在这种情况下,Redis会将失效的数据从缓存中移除。

26、Redis怎么解决大Key问题?

(1)使用分片或分布式存储:如果单台Redis服务器无法容纳大量的大Key,可以考虑使用分片或者将数据分散到多个Redis实例中。这样可以减轻单台服务器的压力,提高整体性能。

(2)使用Stream或List等数据结构:如果数据可以以流的形式处理,可以考虑使用Redis的Stream或List等数据结构来存储数据,这样可以分批次地处理大数据,而不会一次性将整个数据存储在一个Key中。

(3)使用压缩算法:对于一些可以被压缩的数据类型,可以在存储之前先进行压缩,然后在读取时解压缩。Redis本身并不提供压缩功能,但可以在应用层实现压缩和解压缩。

Mysql

数据库篇

1、什么是索引?

索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。

索引的优点:

(1)使用索引可以大大加快数据的检索速度。

(2)通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

索引的缺点:

(1)创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低SQL执行效率。

(2)索引需要使用物理文件存储,也会耗费一定空间。

2、MySQL索引有哪些?

按照数据结构维度划分:

(1)BTree索引:是MySQL默认最常用的索引类型。只有叶子节点存储value,非叶子节点只有指针和key。存储引擎MyISAM和InnoDB实现BTree索引都是使用B+Tree,但二者实现方式不一样。InnoDB引擎中,其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶子节点data域保存了完整的数据记录。

(2)哈希索引:类似键值对的形式,一次即可定位。

(3)全文索引:对文本内容进行分词,进行搜索。目前只有CHAR、VARCHAR、TEXT列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如ElasticSearch代替。

按底层存储方式角度划分:

(1)聚簇索引:索引结构和数据一起存放的索引,InnoDB中的主键索引就属于聚簇索引。

(2)非聚簇索引:索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。

MySQL的MyISAM引擎,不管是主键还是非主键,使用的都是非聚簇索引。

按应用维度划分:

(1)主键索引:加速查询+列值唯一(不可以有NULL)+表中只有一个。

(2)普通索引:仅加速查询。

(3)唯一索引:加速查询+列值唯一(可以有NULL)。

(4)覆盖索引:一个索引包含所有需要查询的字段的值。

(5)联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。

(6)前缀索引:前缀索引只适用于字符串类型的数据,前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。

MySQL 8.x中实现的索引新特性:

(1)隐藏索引:也称为不可见索引,不会被优化器使用,但是仍然需要维护,通常在软删除和灰度发布的场景中使用。主键不能设置为隐藏。

(2)降序索引:之前的版本支持通过desc来指定索引为降序,但实际上创建的仍然是常规的升序索引。直到MYSQL 8.x版本才开始真正支持降序索引。另外,在MySQL 8.x版本中,不再对GROUP BY语句进行隐式排序。

(3)函数索引:从MYSQL 8.0.13版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。

3、B树和B+树的区别?

B树是多路平衡查找树。

(1)B树的所有节点既存放键(key)也存放数据(data),而B+树只有叶子节点存放key和data,其他内节点只存放key。

(2)B树的叶子节点都是独立的,B+树的叶子节点有一条引用链指向与它相邻的叶子节点。

(3)B树的检索过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而B+树的检索效率就很稳定了,任何查找都是从跟节点到叶子节点的过程,叶子节点的顺序检索很明显。

(4)在B树中进行范围查询时,首先找到要查找的下限,然后对B树进行中序遍历,直到找到查找的上限;而B+树的范围查询,只需要对链表进行遍历即可。

4、聚簇索引和非聚簇索引的详细介绍?

聚簇索引即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB中的主键索引就属于聚簇索引。在MySQL中,InnoDB引擎的表的.ibd文件就包含了该表的索引和数据,对于InnoDB引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。

非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。

B+树中,如果为聚簇索引,叶子节点的data域存放数据;如果是非聚簇索引,data将存放指向数据的指针。

聚簇索引的优点:(1)查询速度非常快:聚簇索引的查询速度非常的快,因为整个B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引,聚簇索引少了一读取数据的IO操作。(2)支持排序查找和范围查找优化:聚簇索引对于主键的排序查找和范围查找速度非常快。

缺点:(1)依赖于有序的数据:因为B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或UUID这种又长又难比较的数据,插入或查找的速度肯定比较慢。(2)更新代价大:如果索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。

非聚簇索引的优点:(1)更新代价比聚簇索引要小。非聚簇索引的叶子节点是不存放数据的。

缺点:(1)依赖于有序的数据:跟聚簇索引一样,非聚簇索引也依赖于有序的数据。(2)可能会二次查询(回表):当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。

.myd文件包含表的数据,.myi文件包含表的索引。【MyISAM】

5、哈希索引和B+树索引?

B+树索引:

(1)基本思想:B+树索引是一种多路平衡查找树,它将索引键按顺序存储在树的叶子节点中,并通过内部节点的索引来快速定位到叶子节点。

(2)优点:支持范围查询:B+树索引能够有效支持范围查询,例如大于、小于等条件。支持模糊查询:B+树索引对模糊查询也有良好的支持。适用于范围查询频繁的情况:如果查询中包含了范围查询,B+树索引通常更优。

(3)缺点:相对较大的内存开销:B+树索引相对于哈希索引需要占用更多的内存。查找效率相对于哈希索引略低:B+树索引的查找复杂度通常是对数级别的,即O(log n)。

(4)适用范围:适用于范围查询较多的场景,如区间查询、排序等。适用于数据分布较为平均的情况。

哈希索引:

(1)基本思想:哈希索引是通过将索引键的哈希值映射到一个固定大小的散列表中,快速定位到对应的数据块,从而实现快速检索。

(2)优点:快速查找:在理想情况下,哈希索引的查找效率是常数级别的,即O(1)。适用于等值查询:对于等值查询(如通过主键查找)、连接等操作,哈希索引效果显著。

(3)缺点:不支持范围查询:哈希索引不支持范围查询,例如大于、小于等条件无法直接利用哈希索引进行优化。不适用于模糊查询:对于模糊查询,哈希索引也不起作用,因为哈希函数将相似的键映射到不同的桶中。

(4)适用场景:适用于等值查询较多的场景,如主键查询。适用于内存较小的情况,因为哈希索引通常比B+树索引占用更少的内存。

6、mysql超大分页处理?

进行limit分页查询,需要对数据进行排序,越往后,分页查询的效率越低。

优化思路就是创建覆盖索引,通过覆盖索引加子查询形式进行优化。

6、索引下推是什么?

索引下推是MySQL 5.6版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。

7、索引创建的规则?

(1)被频繁查询的字段。

(2)被作为条件查询的字段。

(3)频繁需要排序的字段。

(4)字符串类型的字段使用前缀索引代替普通索引。

(5)不为NULL的字段。

(6)被经常频繁用于连接的字段。

(7)尽可能的考虑建立联合索引而不是单列索引。

(8)限制每张表上的索引数量。

(9)注意避免冗余索引,冗余索引指的是索引的功能相同,应该尽量扩展已有的索引而不是创建新索引。

(10)删除长期未使用的索引。MySQL 5.7可以通过sys库的schema_unused_indexes视图来查询哪些索引从未被使用。

8、索引失效的情况?

(1)<>不等于会导致索引失效。

(2)不正确的like查询,使用like,like查询以%开头就会失效,以%结尾不会失效;

(3)不符合最左原则,索引的最左边那个条件必须有;

(4)索引列进行了类型转换;

(5)where后使用or导致索引失效;

(6)对索引列进行了计算或使用了函数;

(7)范围查询数据量过多导致索引失效。

9、如何分析语句是否走索引查询?

可以使用EXPLAIN命令来分析SQL的执行计划,这样就知道语句是否命中索引了。执行计划是指一条SQL语句在经过MySQL查询优化器的优化会后,具体的执行方式。

EXPLAIN并不会真的去执行相关的语句,而是通过查询优化器对语句进行分析,找出最优的查询方案,并显示对应的信息。

10、SQL语句执行很慢,如何分析?

(1)通过keykey_len检查是否命中了索引。

(2)通过type字段查看sql是否有进一步的优化空间。

(3)通过extra建议判断,是否出现了回表的情况(添加索引或修改返回字段)。

11、sql优化?

(1)表的设计优化,数据类型选择根据需求来。

(2)索引优化(索引创建原则)。

(3)sql语句优化(不使用select *,避免索引失效,聚合查询多用union all(union会多一次过滤),表关联使用inner join,不使用left join,right join)。

(4)采用主从复制,读写分离的模式,让数据库的写入,不影响查询的效率。

(5)分库分表。

12、事务的特性?

​ 事务是一组操作的集合,不可分割的工作单位。

(1)原子性:事务是不可分割的最小操作单元,要么全部成功,要么全部失败。

(2)一致性:事务完成时,必须使所有数据保持一致状态。

(3)隔离性:保证事务在不受外部并发操作影响的独立环境运行。

(4)持久性:事务一旦提交或回滚,对数据库数据的改变是永久的。

事务一致性是通过什么实现的?

MySQL会使用事务日志进行恢复,将事务重新应用到数据库中,从而保证事务的一致性。回滚日志记录了事务对数据的修改情况,它允许在事务回滚时恢复数据到之前的状态。

事务隔离性是通过什么实现的?

MySQL通过MVCC实现了事务的隔离性,它通过在每行数据上保留多个版本来实现非阻塞的读操作。

MySQL使用锁来实现事务的隔离性,保证同时运行的事务之间相互不干扰。不同的隔离级别会采用不同的锁策略。

13、并发事务带来的问题,怎么解决这些问题?

脏读:一个事务读到另一个事务还没有提交的数据。

不可重复读:一个事务先后读取同一条记录,两次读取的数据不同。

幻读:一个事务按照条件查询数据时,没有对应的数据行,插入数据时,又发现这行数据已经存在。

解决方案:对事务进行隔离。

14、MySQL的隔离级别?

(1)读取未提交内容:最低的隔离级别,允许一个事务读取另一个未提交事务所做的修改,可能导致脏读问题。

(2)读取已提交内容:保证一个事务只能读取已经提交的数据,避免了脏读问题,但可能导致不可重复读问题。

(3)可重复读(默认的):保证一个事务在整个事务过程中读取的数据保持一致,即使其他事务对数据进行了修改。避免了不可重复读问题,但可能会导致幻读问题。

(4)序列化:最高的隔离级别,提供最强的事务隔离。保证同时执行的事务之间不会出现脏读、不可重复读和幻读问题。通过锁机制来实现,可能会导致并发性能下降。

事务隔离级别越高,数据越安全,但是性能越低。

15、解决幻读的方法?

核心思想就是一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据。具体地:

(1)将事务隔离级别调整为序列化。

(2)在可重复读的事务级别下,给事务操作的这张表添加表锁。

(3)在可重复读的事务级别下,给事务操作的这张表添加Next-Key Lock(Record Lock+Gap Lock)。

16、InnoDB的可重复读?

但是InnoDB实现的可重复读隔离级别是可以解决幻读问题发生的,有两种情况:

(1)快照读:由MVCC机制来保证不出现幻读。

(2)当前读:使用Next-Key Lock进行加锁来保证不出现幻读,Next-Key Lock是行锁和间隙锁的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。

17、InnoDB在分布式事务下采用序列化隔离级别?

InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。

分布式事务指的是允许多个独立的事务资源参与到一个全局的事务中。

18、MySQL的存储引擎?

MySQL 5.5.5之前,MyISAM是MySQL的默认存储引擎。5.5.5版本后,InnoDB是Mysql的默认存储引擎。

19、MySQL存储引擎架构了解吗?

MySQL存储引擎采用的是插件式架构,支持多种存储引擎,可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库。

20、MyISAM和InnoDB的区别?

(1)InnoDB支持行级别的锁粒度,MyISAM不支持,只支持表级别的锁粒度。

(2)MyISAM不提供事务支持。InnoDB提供事务支持,实现了SQL标准定义了四个隔离级别。

(3)MyISAM不支持外键,而InnoDB支持。

(4)MyISAM不支持MVCC,而InnoDB支持。

(5)MyISAM不支持数据库异常崩溃后的安全恢复,而InnoDB支持。

(6)虽然MyISAM引擎和InnoDB都是使用B+Tree作为索引结构,但是两者的实现方式不太一样。InnoDB引擎中,其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶子节点data域保存了完整的数据记录。

21、InnoDB对MVCC的实现?

MVCC多版本并发控制,指维护一个数据的多个版本,使得读写操作没有冲突。

InnoDB通过数据行的DB_TRX_ID和Read View来判断数据的可见性,如不可见,则通过数据行的DB_ROLL_PTR找到undo log中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建Read View之前已经提交的修改和该事务本身做的修改。

其实现依赖于数据库记录中的隐式字段,read view,undo log。

(1)隐藏字段。(InnoDB存储引擎为每行数据添加了三个隐藏字段)

  • DB_TRX_ID(事务id、6字节):表示最后一次插入或更新该行的事务id,自增的。
  • DB_ROLL_PTR(7字节):回滚指针,指向该行的undo_log。如果该行未被更新,则为空。
  • DB_ROW_ID(6字节):如果没有设置主键且该表没有唯一非空索引时,InnoDB会使用该id来生成聚簇索引。

(2)ReadView。(解决一个事务查询选择版本的问题,快照读SQL执行是MVCC提取数据的依据,记录并维护系统当前活跃的事务id。)

  • 快照读:简单的select就是快照读,读取的是记录数据的可见版本。
  • 当前读:读取记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

RC(读取已提交)级别下,事务每一次执行快照读时生成readview。

RR(可重读)级别下,仅在事务中第一次执行快照读时生成readview,然后复用readview。

(3)uodo log(回滚日志)。

  • 回滚日志:存储老版本数据。
  • 版本链:不同事务或者相同事务对同一条记录进行修改,该记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,尾部是最早的旧记录。

22、Mysql锁有哪些,如何理解?

按锁粒度分类:

(1)行锁:锁某行数据,锁粒度最小,并发度高,针对索引字段加的锁,只针对当前操作的行记录进行加锁。行级锁和存储引擎有关,是在存储引擎层面实现的。但是加锁的开销大,加锁慢,会出现死锁。

(2)表锁:锁整张表,锁粒度最大,并发度低,针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。表级锁和存储引擎无关,MyISAM和InnoDB引擎都支持表级锁。

(3)间隙锁:锁的是一个区间。

还可以分为:

(1)共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写;

(2)排它锁,也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写。

还可以分为:

(1)乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的。

(2)悲观锁:上面锁的行锁、表锁等都是悲观锁。

在事务的隔离级别实现中,就需要锁来解决幻读。

23、行级锁的使用有什么注意事项?

InnoDB的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。当我们执行UPDATE、DELETE语句时,如果WHERE条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表中的所有行记录进行加锁。

24、InnoDB有哪几类行锁?

InnoDB行锁是通过对索引数据页上的记录加锁实现的,支持三种行锁定方式:

(1)记录锁(Record Lock):属于单个行记录上的锁。

(2)间隙锁(Gap Lock):锁定一个范围,不包括记录本身。

(3)临键锁(Next-Key Lock):Record Lock + Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题。记录锁只能锁住已经存在的记录,为了避免插入新纪录,需要依赖间隙锁。

25、意向锁有什么作用?

意向锁是表级锁,有两种:

(1)意向共享锁:事务有意向对表中的某些记录加共享锁(S锁),加共享锁前必须先取得该表的IS锁。

(2)意向排他锁:事务有意向对表中的某些记录加排他锁(X锁),加排他锁前必须先取得该表的IX锁。

意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB会先获取该数据行所在数据表的对应意向锁。

26、自增锁有了解吗?

如果一个事务正在插入数据到自增列的表时,会先获取自增锁,拿不到就可能会被阻塞住。这里的阻塞行为只是自增锁行为的其中一种,可以理解为自增锁就是一个接口,其具体的实现有多种。

具体的配置项为:innodb_autoinc_lock_mode,可以选择的值如下:

0—传统模式;1—连续模式;2—交错模式。

交错模式下,所有的INSERT-LIKE都不使用表级锁,使用的是轻量级互斥锁实现,多条插入语句可以并发执行,速度更快,扩展性也更好。

如果MySQL数据库有主从同步需求并且Binlog存储格式为Statement的话,不要将InnoDB自增锁模式设置为交叉模式,不然会有数据不一致性问题。这是因为并发情况下插入语句的执行顺序就无法得到保障。

27、锁升级过程,可以降级吗?

(1)悲观锁升级: 在某些情况下,可以将悲观共享锁升级为悲观排他锁。例如,在读多写少的场景中,当需要修改数据时,可以将共享锁升级为排他锁,以防止其他线程的读写操作干扰。

(2)乐观锁升级: 乐观锁一般不涉及锁升级,因为乐观锁的核心思想是通过版本号或时间戳等机制来避免锁竞争,而不需要进行锁的升级。

锁降级的支持取决于具体的锁实现和编程语言。在某些情况下,锁降级是可能的,但在其他情况下可能会更加复杂或不可行。一般来说,锁降级需要确保在释放高级别锁之前,已经获得低级别锁,以避免数据不一致或并发问题。

28、数据库如何降低死锁?

(1)降低事务的隔离级别。

降低事务的隔离级别可以减少死锁的发生,因为事务的隔离级别越高,锁的粒度就越大,这会增加死锁的概率。但是,等级太低可能会引起脏读、不可重复读和幻读等问题,需要根据实际情况权衡。

(2)减少事务并发度。

减少事务并发度也可以减少死锁的发生。当存在大量并发事务时,会增加死锁的概率。可以通过调整业务流程或者更改代码实现。

  • 优化SQL语句和索引。

    优化SQL语句和索引可以减少对同一数据行的竞争,从而降低死锁的概率。可以通过合理设计索引、使用批量更新或者延迟加载等方式来优化SQL语句。

  • 使用数据库的死锁检测和超时机制。

    大多数数据库会提供死锁检测和超时机制,可以使用这些机制来避免或解决死锁问题。当发现死锁时,数据库会自动回滚其中一个事务,释放资源,避免了死锁的进一步扩大。而超时机制则可以在一定时间内主动结束事务,释放资源,避免死锁的长时间持续,从而提高数据库的并发性能。

29、MySQL中三大日志?

(1)binlog二进制日志(归档日志):是逻辑日志,记录内容是语句的原始逻辑,类似于“给ID=2这一行的c字段加1”,属于MySQL的Server层。保证数据的一致性。会记录所有涉及更新数据的逻辑操作,并且是顺序写。

binlog日志有三种格式,可以通过binlog_format参数指定,分别是:statement(记录了SQL语句原文)、row(还包含操作的具体数据)、mixed()。row格式记录的内容看不到详细信息,要通过mysqlbinlog工具解析出来。

(2)redolog重做日志(事务日志):是物理日志,记录内容是“在某个数据页上做了什么修改”,属于InnoDB存储引擎。保证事务的持久性。

(3)undolog回滚日志:保证事务的原子性。记录了事务的撤销操作,它用于在事务回滚时恢复数据到事务开始之前的状态。当事务执行过程中需要回滚或发生错误时,回滚日志被用来还原事务对数据所做的改变。

28、undo log和redo log的区别?

redo log:记录的是事务提交时数据页的物理修改,用来实现事务的持久性

组成:重做日志缓冲和重做日志文件,前者在内存中,后者在磁盘中。

undo log:回滚日志,用于记录数据被修改前的信息,作用包括两个:提供回滚MVCC(多版本并发控制)可以实现事务的一致性和原子性

29、MySQL主从复制原理?

MySQL主从复制的核心就是二进制日志

(1)主库将数据库中数据的变化写入到binlog。

(2)从库连接主库。

(3)从库会创建一个I/O线程向主库请求更新的binlog。

(4)主库会创建一个binlog dump线程来发送binlog ,从库中的I/O线程负责接收。

(5)从库的I/O线程将接收的binlog写入到relay log中。

(6)从库的SQL线程读取relay log同步数据本地(也就是再执行一遍 SQL )。

30、如何实现Mysql读写分离?

(1)部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。

(2)保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的主从复制

(3)系统将写请求交给主数据库处理,读请求交给从数据库处理。

在项目中,常用的方式有两种:

(1)代理方式:可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。提供类似功能的中间件有 MySQL RouterAtlas(基于 MySQL Proxy)、MaxScaleMyCat

(2)组件方式:通过引入第三方组件来帮助我们读写请求。可以使用 sharding-jdbc ,直接引入jar包即可使用,非常方便。同时,也节省了很多运维的成本。

31、主从一致性问题?

(1)强制将读请求路由到主库处理。

既然从库的数据过期了,那就可以直接从主库读取!这种方案虽然会增加主库的压力,但是,实现起来比较简单。

比如 Sharding-JDBC就是采用的这种方案。通过使用Sharding-JDBC的 HintManager 分片键值管理器,我们可以强制使用主库。

在这种方案中,可以将那些必须获取最新数据的读请求都交给主库处理。

(2)延迟读取。

比如主从同步延迟0.5s,那就1s之后再读取数据。

对于一些对数据比较敏感的场景,可以在完成写请求之后,避免立即进行请求操作。比如你支付成功之后,跳转到一个支付成功的页面,当你点击返回之后才返回自己的账户。

32、分库分表?

解决存储压力。

垂直分库:根据业务进行拆分,高并发下提高磁盘IO和网络连接数;实现微服务。

垂直分表:冷热数据分离,多表互不影响。

水平分库:一个库的数据拆分到多库,解决海量数据存储和高并发的问题。

水平分表:解决单表存储和性能问题。

  • 引入分库分表之后,需要系统解决事务、分布式id、无法join操作问题。
  • ShardingSphere的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。另外,ShardingSphere的生态体系完善,文档完善,更新和发布比较频繁。

33、三大范式?

  1. 第一范式(1NF):确保每个数据库表中的每个列都包含原子值,即不可再分。这意味着每个单元格只能包含一个值,不允许多个值的集合、数组或嵌套结构。这有助于减少数据冗余。
  2. 第二范式(2NF):在满足第一范式的基础上,要求表中的每个非主键列都完全依赖于主键,而不是依赖于主键的一部分。这有助于消除部分依赖,确保数据的一致性和完整性。
  3. 第三范式(3NF):在满足第二范式的基础上,要求表中的每个非主键列都不依赖于其他非主键列。这有助于消除传递依赖,确保数据更新异常的减少。

34、动态sql?

可以根据特定的情况动态地生成不同的SQL语句,从而实现灵活的查询。在构建动态SQL时,应该谨慎处理用户输入,避免直接拼接用户输入到SQL语句中。

可能存在SQL注入问题。防止SQL注入的方法有:

(1)使用参数化查询或预编译语句。使用参数化查询可以防止用户输入的数据被解释为SQL代码,不要直接将用户输入拼接到SQL查询中。

(2)限制数据库用户权限。给应用程序连接数据库的用户分配最小必要的权限,避免使用具有过高权限的用户。

(3)输入验证。在应用程序层面对用户输入进行验证,确保他们符合预期的格式和范围。

(4)转义特殊字符。

(5)使用ORM(对象关系映射):ORM框架通常会提供内建的防护机制,可以有效防止SQL注入。

35、MySQL的分片算法?

分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。

(1)哈希分片:求指定key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。

(2)范围分片:按照特性的范围区间(比如时间区间、ID 区间)来分配数据,比如将 id1~299999 的记录分到第一个库, 300000~599999 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。

(3)地理位置分片:很多NewSQL数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。

(4)融合算法:灵活组合多种分片算法,比如将哈希分片和范围分片组合。

35、MySQL的对象有哪些?

数据库、表、视图(视图是一个虚拟表,其内容是一个查询定义,可以简化复杂的查询操作,也可以用于控制用户对数据的访问权限)、函数、索引。

36、#{}和${}的区别是什么?

#{}是预编译处理,是占位符,${}是字符串替换、是拼接符。

Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement来赋值。

Mybatis在处理${}时,会将sql中的{}替换成变量的值,调用Statement来赋值。

使用#{}可以有效的防止SQL注入,提高系统安全性。

37、Mybatis的批处理?

(1)首先编写Mapper文件,在Mapper文件中编写SQL语句,需要使用foreach标签来循环执行批处理的SQL。

(2)然后编写DAO接口方法,在DAO接口中定义批处理的方法,方法的参数应该是一个List,其中包含了需要批处理的对象。

(3)在Mybatis的配置文件中,需要添加批处理选项。

(4)在代码中调用批处理方法,并传入需要批处理的数据列表。

批处理通常用于批量插入、更新等操作,可以有效地减少数据库访问次数,提升性能。

38、MyBatis怎么防止sql注入?

(1)使用参数绑定:可以确保用户输入的数据不会直接拼接到SQL查询中,而是通过参数的方式传递给数据库。

应该用#{}。

(2)不要使用字符串拼接来构建SQL查询。

String sql = “SELECT * FROM users WHERE username = ‘“ + userInput + “‘“;

(3)动态SQL标签,可以根据条件来动态生成SQL查询语句。这样可以避免不必要的拼接。

(4)使用预编译语句:如果在Java代码中直接操作数据库,确保使用预编译语句。这可以防止直接在SQL查询中插入恶意代码。

(5)在接受用户输入之前,进行输入验证和过滤,以确保输入符合预期格式和范围。

(6)避免直接将用户输入传递给动态SQL,而是先进行验证和过滤,然后再将参数传递给MyBatis。

408

操作系统

1、页面置换算法?

主要用于在物理内存不足时,决定哪些页面(内存中的一段数据)应该被换出到磁盘上,以便为新的页面腾出空间。

(1)先进先出(FIFO): 总是选择最早进入内存的页面进行置换。可能导致增加页面数时,缺页次数反而增加。

(2)最近最久未使用: 也就是最近最久没有被访问的页面应该被置换。这需要维护一个页面访问历史记录,可能需要较大的开销。

(3)最少使用: 也就是最少被访问的页面应该被置换。它需要维护每个页面的使用频率信息。

(4)时钟: 时钟算法维护一个环形链表,模拟一个时钟。每个页面都有一个”访问位”,当页面被访问时,访问位被设置为1。当需要置换页面时,算法从时钟指针位置开始扫描,找到一个访问位为0的页面进行置换。

(5)最佳: 总是置换将来最长时间内不会被访问的页面。然而,实际中很难实现,因为需要未来访问模式的信息。

2、项目部署怎么分配的?从内存、线程等方面谈谈?

在内存分配方面:

(1)预估内存需求: 首先需要估算应用程序所需的内存。考虑应用程序的规模、并发用户数、缓存需求等。可以通过性能测试、基准测试等手段来获取数据。

(2)分配堆内存和非堆内存: Java 应用程序通常使用堆内存和非堆内存。堆内存主要用于存储对象实例,需要根据预估的对象数量和大小来分配。非堆内存主要用于类加载、元数据等,也需要适当分配。

(3)垃圾回收配置: 配置垃圾回收参数,如堆内存大小、垃圾回收算法、垃圾回收的触发阈值等。不同的应用场景可能需要不同的垃圾回收策略。

在线程分配方面:

(1)并发用户数和任务: 确定应用程序需要同时处理的并发用户数和任务数。根据这些需求,估算所需的线程数量。

(2)线程池配置: 使用线程池来管理线程资源。设置线程池的核心线程数、最大线程数、任务队列大小等参数,以及适当的拒绝策略。

3、多线程并发会产生什么问题?

(1)死锁: 多个线程互相等待对方释放资源,导致所有线程都无法继续执行。这种情况可能发生在使用多个锁的情况下,导致资源被锁住,无法释放。

(2)饥饿: 一个或多个线程无法获取所需的资源,导致它们无法继续执行。这可能是由于某些线程一直占用资源,导致其他线程无法获得机会。

(3)多个线程并发访问共享资源,如果没有适当的同步措施,可能会导致数据不一致或错误的结果。例如,多个线程同时修改同一个变量可能会导致意外的结果。

(4)数据一致性问题: 在多线程环境下,确保数据的一致性可能会变得更加复杂。需要适当地处理数据的读写操作,以确保数据的正确性。

4、线程间同步的方式有哪些?

(1)互斥锁:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。

(2)读写锁:允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。

(3)信号量:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。

(4)屏障:屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 CyclicBarrier 是这种机制。

(5)事件 :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

5、进程的调度算法有哪些?

(1)先到先服务调度算法(FCFS) : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。

(2)短作业优先的调度算法(SJF) : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。

(3)时间片轮转调度算法(RR) : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

(4)优先级调度算法(Priority):为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。

(5)多级反馈队列调度算法(MFQ):前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。

6、虚拟内存?

虚拟内存本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。

虚拟内存主要提供了下面这些能力:

(1)隔离进程:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。

(2)提升物理内存利用率:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。

(3)简化内存管理:进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。

(4)多个进程共享物理内存:进程在运行过程中,会加载许多操作系统的动态库。这些库对于每个进程而言都是公用的,它们在内存中实际只会加载一份,这部分称为共享内存。

(5)提高内存使用安全性:控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。

(6)提供更大的可使用内存空间:可以让程序拥有超过系统物理内存大小的可用内存空间。这是因为当物理内存不够用时,可以利用磁盘充当,将物理内存页(通常大小为 4 KB)保存到磁盘文件(会影响读写速度),数据或代码页会根据需要在物理内存与磁盘之间移动。

7、没有虚拟内存有什么问题?

如果没有虚拟内存的话,程序直接访问和操作的都是物理内存,看似少了一层中介,但多了很多问题。比如:

(1)用户程序可以访问任意物理内存,可能会不小心操作到系统运行必需的内存,进而造成操作系统崩溃,严重影响系统的安全。

(2)同时运行多个程序容易崩溃。比如你想同时运行一个微信和一个 QQ 音乐,微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就可能会造成微信这个程序会崩溃。

(3)程序运行过程中使用的所有数据或指令都要载入物理内存,根据局部性原理,其中很大一部分可能都不会用到,白白占用了宝贵的物理内存资源。

8、什么是虚拟地址和物理地址?

物理地址是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。程序中访问的内存地址不是物理地址,而是虚拟地址

也就是说,我们编程开发的时候实际就是在和虚拟地址打交道。比如在C语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的虚拟地址。

操作系统一般通过CPU芯片中的一个重要组件 MMU(Memory Management Unit,内存管理单元) 将虚拟地址转换为物理地址,这个过程被称为 地址翻译/地址转换(Address Translation)

9、什么是虚拟地址空间和物理地址空间?

(1)虚拟地址空间是虚拟地址的集合,是虚拟内存的范围。每一个进程都有一个一致且私有的虚拟地址空间。

(2)物理地址空间是物理地址的集合,是物理内存的范围。

10、虚拟地址与物理内存地址是如何映射的?

MMU将虚拟地址翻译为物理地址的主要机制有3种:(1)分段机制(2)分页机制(3)段页机制。

(1)分段机制 以段的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段MAIN、子程序段X、数据段D及栈段S等。

(2)分页机制 把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。现代操作系统广泛采用分页机制。

(3)断页机制是结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。

在段页式机制下,地址翻译的过程分为两个步骤:段式地址映射和页式地址映射。

计算机网络

1、TCP的三次握手和四次挥手?

TCP协议是7层网络协议中的传输层协议,负责数据的可靠传输。

建立TCP连接时,需要通过三次握手来建立,过程是:

(1)客户端向服务器端发送一个SYN;

(2)服务端接收到SYN后,给客户端发 送一个SYN_ACK;

(3)客户端接收到SYN_ACK后,再给服务端发送一个ACK。

断开TCP连接时,需要通过四次挥手来断开,过程是:

(1)客户端向服务端发送FIN;

(2)服务端接收FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端可以不发数据了,不过服务端这边可能还有数据正在处理;

(3)服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接;

(4)客户端接收到服务端的FIN,向服务端发送ACK,表示客户端也会断开连接。

2、ISO七层模型?

(1)物理层: 是网络的最底层,主要关注物理传输媒介和数据的传输方式等。主要作用是提供原始的比特流传输,确保数据在传输媒介上的可靠传输。网线、集线器、中继器、转接器等。

(2)数据链路层: 负责将物理层传输的数据帧组织成逻辑上的数据帧,并提供了点对点通信的逻辑连接。它还负责流量控制、错误检测和纠正,以及介质访问控制。

对应的硬件有:网关、二级交换机、网桥、无线接入点等。

(3)网络层: 处理数据在不同主机之间的路由和转发,决定数据如何在网络中传输。它负责寻址、路由选择、分段和重组,以及网络拓扑的管理。路由器、三层交换机、防护墙等。

(4)传输层: 提供了端到端的通信,负责数据的分段、传输控制和流量控制。常见的传输层协议有TCP和UDP。

(5)会话层: 管理不同主机之间的会话或对话,为应用程序提供数据交换的逻辑结构。它处理会话的建立、维护和结束,以及数据同步和检查点的管理。

(6)表示层: 负责数据的格式转换、加密和压缩,以确保数据在不同主机之间的传输和解释。它处理数据的语法和语义转换,以便应用程序能够正确解释数据。

(7)应用层: 应用层是最高层,提供了用户与网络之间的接口,为应用程序提供网络服务。它包含了各种网络应用协议,如 HTTP、FTP、SMTP、DNS 等,用于实现各种网络应用,例如网页浏览、电子邮件、文件传输等。

3、GET和POST的区别?

(1)GET:数据会附加在URL的查询字符串中,即放在URL的后面,通过?分隔,参数名和参数值用&连接。

​ POST:数据会包含在请求的消息体中,而不会直接暴露在URL中。

(2)GET:由于数据放在URL中,所以受到浏览器和服务器对URL长度的限制,通常较短,约为2048个字符。

​ POST:没有特定的数据长度限制,可以发送较大的数据量。

(3)GET:默认情况下,GET请求可能会被浏览器缓存,重复请求相同URL时,可能会返回缓存的响应。

​ POST:POST请求默认不会被浏览器缓存。

(4)GET:适用于获取数据,不会对服务器端数据产生影响,比如查询数据、获取资源。

​ POST:适用于提交数据,可能对服务器端数据产生影响,比如提交表单、上传文件等。

4、Http和Https的区别?

HTTP:是互联网上应用最广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输。

HTTPS:是HTTP的加强版,可以认为是HTTP+SSL(Secure Socket Layer)。在HTTP的基础上增加了一系列的安全机制。一方面保证数据传输安全,另一方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。

主要区别:

(1)HTTP的连接是简单无状态的,HTTPS的数据传输是经过证书加密的,安全性更高。

(2)HTTP是收费的,HTTPS需要申请证书,而证书通常是需要收费的,并且费用一般不低。

(3)他们的传输协议不同,所以他们使用的端口也是不一样的,HTTP默认是80端口,而HTTPS默认是443端口。

HTTPS的缺点:

(1)HTTPS的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。

(2)HTTPS也并不是完全安全的。因为他的证书体系其实并不是完全安全的。并且HTTPS在面对DDOS这样的攻击时,几乎不起任何作用。

(3)证书需要费钱,并且功能越强大的证书费用越高。

5、HTTPS的工作流程?

(1)客户端请求: 用户在浏览器中输入一个 HTTPS 的 URL,浏览器会向服务器发起一个 HTTPS 请求。

(2)服务器证书: 服务器将自己的数字证书发送给客户端,该证书包含了服务器的公钥以及一些其他信息,如证书的颁发机构、有效期等。

(3)客户端验证证书: 客户端接收到服务器的证书后,会首先验证证书的合法性。它会检查证书是否由受信任的证书颁发机构签名,以及是否在有效期内。如果证书无效或不被信任,浏览器会弹出警告。

(4)密钥协商: 如果证书有效并通过验证,客户端会生成一个随机的对称密钥,并使用服务器的公钥进行加密,然后将加密后的密钥发送给服务器。

(5)服务器解密: 服务器接收到客户端发送的加密密钥后,使用自己的私钥进行解密,得到对称密钥。

(6)数据加密: 一旦双方都拥有了相同的对称密钥,后续的通信将使用该密钥进行加密和解密。客户端和服务器之间传输的数据会使用对称加密算法(如AES)进行加密。

(7)数据传输: 客户端和服务器之间传输加密后的数据,即使被截获,也无法轻易解密。

(8)数据解密: 服务器接收到加密数据后,使用对称密钥进行解密,得到原始数据。

(9)响应数据: 服务器将响应数据加密后发送给客户端,客户端使用对称密钥解密,得到原始数据。

6、Cookie和Session的区别?

当服务器tomcat第一次接收到客户端的请求时,会开辟一块独立的session空间,建立一个session对象,同时会生成一个session id,通过响应头的方式保存到客户端浏览器的cookie当中。以后客户端的每次请求,都会在请求头部带上这个session id,这样就可以对应上服务端的一些会话的相关信息,比如用户的登录状态。

当服务端从单体应用升级为分布式之后,cookie+session这种机制要怎么扩展?

(1)session黏贴:在负载均衡中,通过一个机制保证同一个客户端的所有请求都会转发到同一个tomcat实例当中。问题:当这个tomcat实例出现问题之后,请求就会被转发到其他实例,这时候用户的session信息就丢了。

(2)session复制:当一个tomcat实例上保存了session信息后,主动将session复制到集群中的其他实例。问题:复制是需要时间的,在复制过程中,容易产生session信息丢失。

(3)session共享:就是将服务端的session信息保存到一个第三方中,比如Redis。

7、TCP如何保证传输的可靠性?

(1)基于数据块传输:应用数据被分割成TCP认为最适合发送的数据块,再传输给网络层,数据块被称为报文段或段。

(2)对失序数据包重新排序以及去重:TCP为了保证不发生丢包,就给每个包一个序列号,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据就可以实现数据包去重。

(3)校验和 : TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。

(4)超时重传 : 当发送方发送数据之后,它启动一个定时器,等待目的端确认收到这个报文段。接收端实体对已成功收到的包发回一个相应的确认信息(ACK)。如果发送端实体在合理的往返时延(RTT)内未收到确认消息,那么对应的数据包就被假设为已丢失open in new window并进行重传。

(5)流量控制 : TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议(TCP利用滑动窗口实现流量控制)。

(6)拥塞控制 : 当网络拥塞时,减少数据的发送。

8、TCP如何实现流量控制?

TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为0,则发送方不能发送数据。

为什么需要流量控制? 这是因为双方在通信的时候,发送方的速率与接收方的速率是不一定相等,如果发送方的发送速率太快,会导致接收方处理不过来。如果接收方处理不过来的话,就只能把处理不过来的数据存在 接收缓冲区里。如果缓存区满了发送方还在狂发数据的话,接收方只能把收到的数据包丢掉。出现丢包问题的同时又疯狂浪费着珍贵的网络资源。因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种动态平衡才好。

TCP为全双工通信,双方可以进行双向通信,客户端和服务端既可能是发送端又可能是服务端。因此,两端各有一个发送缓冲区与接收缓冲区,两端都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制(TCP 传输速率不能大于应用的数据处理速率)。通信双方的发送窗口和接收窗口的要求相同。

TCP 发送窗口可以划分成四个部分

(1)已经发送并且确认的 TCP 段(已经发送并确认);

(2)已经发送但是没有确认的 TCP 段(已经发送未确认);

(3)未发送但是接收方准备接收的 TCP 段(可以发送);

(4)未发送并且接收方也并未准备接受的 TCP 段(不可发送)。

TCP 接收窗口可以划分成三个部分

(1)已经接收并且已经确认的 TCP 段(已经接收并确认);

(2)等待接收且允许发送方发送 TCP 段(可以接收未确认);

(3)不可接收且不允许发送方发送 TCP 段(不可接收)。

接收窗口的大小是根据接收端处理数据的速度动态调整的。 如果接收端读取数据快,接收窗口可能会扩大。 否则,它可能会缩小。

9、TCP的拥塞控制怎么实现的?

TCP发送方维持一个拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。

TCP的拥塞控制采用了四种算法,即慢开始拥塞避免快重传快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。

(1)慢开始: 是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为 1,每经过一个传播轮次,cwnd加倍。

(2)拥塞避免: 是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT就把发送方的cwnd加 1。

(3)快重传与快恢复: 在TCP/IP中,快速重传和恢复是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。当有单独的数据包丢失时,快速重传和恢复能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。

10、ARQ 协议?

自动重传请求(ARQ)是 OSI 模型中数据链路层和传输层的错误纠正协议之一。通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认信息,它通常会重新发送,直到收到确认或者重试超过一定的次数。ARQ包括停止等待ARQ协议和连续ARQ协议。

停止等待ARQ协议是为了实现可靠传输,基本原理就是每发完一个分组就停止发送,等待对方确认(回复 ACK)。如果过了一段时间(超时时间后),还是没有收到ACK确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组。在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。

1) 无差错情况:

发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。

2) 出现差错情况(超时重传):

停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。

3) 确认丢失和确认迟到

  • 确认丢失:确认消息在传输过程丢失。当 A 发送 M1 消息,B 收到后,B 向 A 发送了一个 M1 确认消息,但却在传输过程中丢失。而 A 并不知道,在超时计时过后,A 重传 M1 消息,B 再次收到该消息后采取以下两点措施:1. 丢弃这个重复的 M1 消息,不向上层交付。 2. 向 A 发送确认消息。(不会认为已经发送过了,就不再发送。A 能重传,就证明 B 的确认消息丢失)。
  • 确认迟到:确认消息在传输过程中迟到。A 发送 M1 消息,B 收到并发送确认。在超时时间内没有收到确认消息,A 重传 M1 消息,B 仍然收到并继续发送确认消息(B 收到了 2 份 M1)。此时 A 收到了 B 第二次发送的确认消息。接着发送其他数据。过了一会,A 收到了 B 第一次发送的对 M1 的确认消息(A 也收到了 2 份确认消息)。处理如下:1. A 收到重复的确认后,直接丢弃。2. B 收到重复的 M1 后,也直接丢弃重复的 M1。

连续 ARQ 协议:可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

11、从输入URL到页面展示到底发生了什么??

(1)DNS解析。

(2)建立TCP连接。

(3)发送HTTP请求。

(4)服务器处理请求并返回HTTP报文。

(5)浏览器解析渲染页面。

(6)连接结束。

12、HTTP到Server如何处理的,在disptchserverlt之前tomcat容器怎么处理的?

  1. Tomcat容器之前的处理:在 Tomcat 容器之前,通常还有一些服务器软件或代理服务器,如 Apache HTTP Server、Nginx 等。这些服务器可以用于负载均衡、反向代理、SSL 加密等,然后将请求转发给 Tomcat 或其他后端服务器。

  2. Tomcat处理请求:一旦请求到达 Tomcat 服务器,Tomcat 就会使用其内部的 Servlet 容器来处理请求。具体流程如下:

    a. Request 对象创建:Tomcat 会解析客户端请求,创建一个 HttpServletRequest 对象,包含了请求的信息(如 URL、HTTP 方法、请求参数等)。

    b. 选择 Servlet:根据请求的 URL 路径,Tomcat 决定要调用哪个 Servlet 来处理请求。这个选择是根据部署的 Web 应用中的配置和映射关系来进行的。

    c. Servlet 处理:选定的 Servlet 被实例化并调用其 service 方法,将 HttpServletRequest 和 HttpServletResponse 对象传递给它。Servlet 可以读取请求参数、执行业务逻辑、生成响应等。

    d. 生成响应:Servlet 处理完请求后,生成一个 HttpServletResponse 对象,设置响应的状态码、头部信息和内容。这个响应会包含服务器生成的数据,如 HTML、JSON 等。

    e. Response 返回:Tomcat 将生成的响应返回给客户端,客户端浏览器会显示或处理响应内容。

  3. Tomcat 容器之后的处理:一旦 Tomcat 生成了响应并返回给客户端,如果在前面有代理服务器,它可能会将响应转发给客户端。客户端浏览器会解析响应并显示内容。

13、服务器端如何实现请求调用?

(1)接收请求:服务器首先需要监听指定的端口,等待客户端发起连接请求。一旦有客户端连接,服务器就会接收到一个连接对象。

(2)解析请求:服务器需要解析客户端发送的请求消息,通常包括请求的类型(GET、POST等)、请求的路径、请求参数等信息。

(3)处理请求:根据请求的内容,服务器端会执行相应的业务逻辑或调用相应的功能模块来处理请求。这可能包括数据库查询、计算、文件操作等。

(4)生成响应:服务器根据处理请求的结果,生成相应的响应消息,通常包括响应的状态码、响应头部信息以及响应体(即返回给客户端的数据)。

(5)发送响应:将生成的响应消息发送给客户端,使其能够获取到相应的结果。

(6)关闭连接:在完成响应后,服务器会关闭与客户端的连接,释放相关资源。

14、HTTP/1.0和 HTTP/1.1有什么区别?

image-20231105192843094

15、HTTP/1.1和HTTP/2.0有什么区别?

image-20231105192919855

16、HTTP/2.0和 HTTP/3.0有什么区别?

image-20231105192935281

17、HTTP状态码有哪些?

image-20231105193025545

18、DNS的作用是什么?

DNS域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS要解决的是域名和IP地址的映射问题。目前DNS的设计采用的是分布式、层次数据库结构,DNS是应用层协议,它可以在UDP或TCP协议之上运行,端口为53

19、DNS的工作流程?

DNS 的查询解析过程分为两种模式:迭代递归

(1)用户发起域名查询:用户在浏览器中输入一个域名(如www.example.com),浏览器会首先检查本地缓存中是否有该域名对应的IP地址。

(2)本地缓存查找:如果本地缓存中有对应的IP地址,则直接返回给浏览器,不需要进行后续的查询过程。

(3)递归查询:如果本地缓存中没有对应的IP地址,本地DNS服务器会向根域名服务器发送一个递归查询请求,询问根域名服务器该域名的解析情况。

(4)根域名服务器响应:根域名服务器收到查询请求后,会根据域名的顶级域名(TLD)返回一个指向TLD域名服务器的响应。

(5)TLD域名服务器查询:本地DNS服务器接收到根域名服务器的响应后,会向TLD域名服务器发送一个查询请求,询问该域名的权威域名服务器是哪一个。

(6)权威域名服务器查询:TLD域名服务器返回一个指向权威域名服务器的响应。本地DNS服务器接着向权威域名服务器发送一个查询请求,询问该域名对应的IP地址。

(7)权威域名服务器响应:权威域名服务器收到查询请求后,会返回域名对应的IP地址。

(8)本地DNS服务器缓存:本地DNS服务器将从权威域名服务器获得的IP地址存储在缓存中,以备下次查询时使用。

(9)返回IP地址给用户:本地DNS服务器将获得的IP地址返回给用户的浏览器。

(10)浏览器发起连接:浏览器使用获得的IP地址建立与目标服务器的连接,然后请求相应的网页或资源。

20、DNS服务器有哪些?

DNS服务器自底向上可以依次分为以下几个层级(所有DNS服务器都属于以下四个类别之一):

(1)根DNS服务器。根DNS服务器提供TLD服务器的IP地址。目前世界上只有13组根服务器,我国境内目前仍没有根服务器。

(2)顶级域DNS服务器(TLD 服务器)。顶级域是指域名的后缀,如comorgnetedu等。国家也有自己的顶级域,如ukfrca。TLD 服务器提供了权威DNS服务器的IP地址。

(3)权威DNS服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的DNS记录,这些记录将这些主机的名字映射为IP地址。

(4)本地DNS服务器。每个ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构。

21、PING命令的工作原理是什么?

PING基于网络层的ICMP(互联网控制报文协议),其主要原理就是通过在网络上发送和接收ICMP报文实现的。

ICMP报文中包含了类型字段,用于标识 ICMP 报文类型。ICMP 报文的类型有很多种,但大致可以分为两类:

  • 查询报文类型:向目标主机发送请求并期望得到响应。
  • 差错报文类型:向源主机发送错误信息,用于报告网络中的错误情况。

PING用到的 ICMP Echo Request(类型为 8 ) 和 ICMP Echo Reply(类型为 0) 属于查询报文类型 。

  • PING 命令会向目标主机发送ICMP Echo Request。
  • 如果两个主机的连通性正常,目标主机会返回一个对应的ICMP Echo Reply。

22、TCP和UDP的区别?

(1)UDP在传送数据之前不需要先建立连接。而TCP提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。

(2)TCP是面向字节流的,UDP是面向报文的。

(3)TCP首部开销(20~60字节)比UDP首部开销(8字节)要大。

(4)由于使用TCP进行传输的时候多了连接、确认、重传等机制,所以TCP的传输效率要比UDP低很多。

(5)TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多。

(6)远地主机在收到UDP报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP提供可靠的传输服务,TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过TCP连接传输的数据,无差错、不丢失、不重复、并且按序到达。

Spring系列

1、Spring、SpringMVC和SpringBoot的区别?

Spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更方便将不同类不同方法中的共同处理抽取成切面、自动注入给方法执行,比如日志、异常等。

SpringMVC是Spring对Web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接收请求,然后定义了一套路由策略及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端。

SpringBoot是Spring提供的一个快速开发工具包,让程序员能更方便、更快速的开发spring+springMVC应用,简化了配置,整合了一系列的解决方案(starter机制)、redis、mongodb、es,可以开箱即用。

2、Spring事务?

Spring事务是Spring框架提供的一种机制,用于管理数据库操作的事务性。事务是一组数据库操作,要么全部成功提交,要么全部回滚,以保持数据的一致性和完整性。Spring 事务管理允许您以声明式或编程式的方式管理务,从而确保在应用程序中执行的一系列操作被视为一个原子性的操作单元。

Spring 提供了多种事务管理策略,包括:

(1)声明式事务管理: 通过配置文件或注解来定义事务的范围和属性,将事务管理与业务逻辑分离,使代码更加清晰。

(2)编程式事务管理: 在代码中显式地控制事务的开始、提交和回滚,给开发者更大的灵活性。

3、Spring容器启动流程是怎样的?

(1)首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中;

(2)然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建;

(3)利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始后这一步骤中;

(4)单例Bean创建完了之后,Spring会发布一个容器启动事件;

(5)Spring启动结束;

(6)在源码中会更复杂,比如源码中会提供一些模板方法,让子类实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BeanFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的。

(7)在Spring启动过程中还会去处理@Import等注解。

4、Spring事务传播机制?

多个事务方法相互调用时,事务如何在这些方法间传播,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。

(1)REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务。

(2)SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行。

(3)MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。

(4)REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。

(5)NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务。

(6)NEVER:不使用事务,如果当前事务存在,则抛出异常。

(7)NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)。

5、Spring事务什么时候会失效?

Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有如下几种:

(1)发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身!

解决方法很简单,让那个this变成UserService的代理类即可!

(2)方法不是public的:@Transactional只能用于public的方法上,事务不会失效,如果要用在非public方法上,可以开启AspectJ代理模式。

(3)数据库不支持事务。

(4)没有被Spring管理。

(5)异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)。

6、Spring用到了哪些设计模式?

(1)工厂模式:BeanFactory、FactoryBean、ProxyFactory。

(2)原型模式:原型Bean、PrototypeTargetSource。

(3)单例模式:单例Bean、SingletonTargetSource。

(4)构建器模式:BeanDefinitionBuilder、StringBuilder。

(5)适配器模式:ApplicationListenerMethodAdapter、AdvisorAdapter。

(6)访问者模式:PropertyAccessor、MessageSourceAccessor。

(7)装饰器模式:BeanWrapper、HttpRequestWrapper。

(8)代理模式:AOP、@Configuration、@Lazy。

(9)观察者模式:ApplicationListener、AdvisedSupportListener。

(10)策略模式:BeanNameGenerator、InstantiationStrategy。

(11)模板方法模式:AbstractApplicationContext。

(12)责任链模式:DefaultAdvisorChainFactory。

7、Spring中的Bean是线程安全的吗?

Spring本身并没有针对Bean做线程安全的处理,所以:

(1)如果Bean是无状态的,那么Bean则是线程安全的。

(2)如果Bean是有状态的,那么Bean则不是线程安全的。

另外,Bean不是线程安全的,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。

8、Spring中Bean创建的生命周期有哪些步骤?

Spring中一个Bean的创建大概分为以下几个步骤:

(1)推断构造方法;

(2)实例化;

(3)依赖注入;

(4)处理Aware回调;

(5)初始化前,处理@PostConstrut注解;

(6)初始化,处理InitializingBean接口;

(7)初始化后,进行AOP。

9、Spring中的事务是如何实现的?

(1)Spring事务底层是基于数据库事务和AOP机制的;

(2)首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean;

(3)当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解;

(4)如果加了,那么则利用事务管理器创建一个数据库连接;

(5)并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步;

(6)然后执行当前方法,方法中会执行sql;

(7)执行完当前方法后,如果没有出现异常就直接提交事务;

(8)如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务;

(9)Spring事务的隔离级别对应的就是数据库的隔离级别;

(10)Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的;

(11)Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql。

10、transaction注解怎么实现原理AOP切面编程,动态代理如何实现?

  1. AOP 切面实现事务注解

    在 Spring 中,AOP 允许将横切关注点(如事务管理)从应用程序的业务逻辑中分离出来,以提高代码的模块化和可维护性。事务注解就是通过 AOP 切面来实现的。基本流程如下:

    a. 定义事务注解:你可以使用 Spring 提供的 @Transactional 注解来标记需要事务管理的方法。

    b. 配置 AOP 切面:在 Spring 配置中,使用切面配置来指定在哪些方法上应用事务。这些切面可以使用 @Around 等注解定义切入点和增强逻辑。

    c. 事务管理器:Spring 会自动创建一个事务代理,该代理会拦截标记有 @Transactional 注解的方法的调用。在方法调用前后,事务管理器会开启、提交或回滚事务。

  2. 动态代理实现事务注解

    动态代理是实现 AOP 的一种方式,它可以在运行时动态地创建代理对象,将横切关注点注入到被代理对象的方法中。在 Spring 的事务管理中,动态代理用于在事务注解标记的方法上织入事务管理逻辑。基本流程如下:

    a. 创建代理对象:当一个 Spring Bean 标记有 @Transactional 注解时,Spring 将创建一个代理对象,该对象包装了原始的 Bean 对象。代理对象会截获方法调用,以实现事务管理。

    b. 方法调用拦截:当调用被代理对象的方法时,代理对象会截获方法调用,并在方法调用前后执行额外的逻辑,如开启事务、提交事务、回滚事务等。

    c. 事务管理器:动态代理会与事务管理器进行交互,根据方法的执行情况决定是否提交或回滚事务。

11、Spring的优缺点是什么

方便解耦,简化开发。

​ 通过Spring提供的IOC容器,可以集中管理对象,降低对象和对象之间的耦合度,方便维护对象。有了Spring,用户不必再为单实例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。

AOP编程的支持。

​ 在不修改代码的情况下可以对业务代码进行增强,减少重复代码,提高开发效率,维护方便。比如,事务处理、日志管理、权限控制等。

声明事务的支持。

​ 提高开发效率,只需要一个简单注解@Transactional。

方便程序的测试。

​ Spring实现测试,可以结合junit非常方便测试Spring中的Bean、SpringMVC接口。

方便集成各种优秀框架。

​ 拥有非常强大的粘合度,集成能力非常强,只需要简单配置就可以集成第三方框架。

降低Java EE API的使用难度。

​ 简化了开发,帮我们封装了很多功能性代码,比如JDBC、JavaMail、远程调用等。

Java源码是经典学习范例。

​ 学习Spring底层的实现、反射、设计模式,提供了非常多的拓展接口供外部进行拓展。

12、Spring IOC容器是什么?有什么作用?

1
UserService service = new UserService(); //耦合度太高,维护不方便

​ 引入IOC,将创建对象的控制权交给Spring的IOC,以前由程序员自己控制对象创建,现在交给Spring的IOC去创建,如果要去使用对象需要通过DI(依赖注入)@Autowried自动注入 就可以使用对象。

​ 优点:(1)集中管理对象,方便维护。(2)降低耦合度。

​ 优点:(1)IOC容器支持加载服务时的饿汉式初始化和懒加载。

​ (2)最小的代价和最小的侵入性使松散耦合得以实现。

13、Spring IoC 的实现机制是什么?

Spring 中的 IoC 的实现机制就是工厂模式加反射机制。

14、IOC和DI的区别是什么?

IOC是依赖倒置原则的设计思想,而DI是具体的实现方式。

15、紧耦合和松耦合有什么区别?

紧密耦合是指类之间高度依赖。

松耦合是通过促进单一职责、接口分离、依赖倒置的设计原则来实现的。

16、BeanFactory的作用?

​ • BeanFactory是Spring中非常核心的一个顶层接口。

​ • 它是Bean的工厂,主要是职责就是生产Bean。

​ • 它实现了简单工厂的设计模式,通过调用getBean传入标识生产一个Bean。

​ • 它有非常多的实现类、每个工厂都有不同的职责(单一职责)功能,最强大的工厂是:DefaultListableBeanFactory。Spring底层就是使用该实现工厂进行生产Bean的。

​ • BeanFactory也是容器。 Spring容器(管理Bean的生命周期)。

17、BeanDefinition的作用?

它主要负责存储Bean的定义信息:决定Bean的生产方式。

如:spring.xml

1
2
3
<bean class="com.tuling.User" id="user" scope="singleton" lazy="false" abstract="false" autowire="none">
<property name="username" value="xushu">
</bean>

后续BeanFactory根据这些信息就能生产Bean,比如实例化,可以通过class进行反射进而得到实例对象,比如lazy,则不会在ioc加载时创建Bean。

18、BeanFactory和ApplicationContext有什么区别?

BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当作Spring的容器,都可以管理Bean的生命周期。其中ApplicationContext是BeanFactory的子接口,实现了BeanFactory。

BeanFactory是Spring里面最顶层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化、控制bean的生命周期,维护bean之间的依赖关系。BeanFactory简单粗暴,可以理解为就是个HashMap,Key是BeanName,Value是Bean实例。通常只提供注册(put),获取(get)这两个功能。我们可以称之为“低级容器”。

​ • BeanFactory中getBean用于生产bean。优点:内存占用率小,可以用于嵌入式设备。

​ • ApplicationContext实现了BeanFactory。它不生产Bean而是通知BeanFactory来进行生产,getBean是一个门面方法。做的事情更多,比如:

​ (1)会自动帮我们把配置的bean注册进来

​ (2)加载环境变量;

​ (3)支持多语言;

​ (4)实现事件监听;

​ (5)注册很多对外扩展点

19、BeanFactory和FactoryBean有什么区别?

BeanFactory是一个工厂,也就是一个容器,是来管理和生产bean的。

FactoryBean是一个bean,但它是一个特殊的bean,也由BeanFactory来管理的。

• 它是一个接口,必须被一个bean去实现。

​ • 不是一个普通的Bean,会表现出工厂模式的样子,是一个能产生或者修饰对象生成的工厂Bean,里面的getObject()就是用来获取FactoryBean产生的对象。在BeanFactory中使用”&”来得到FactoryBean本身,用来区分通过容器获取FactoryBean产生的对象还是获取FactoryBean本身。

20、IOC容器的加载过程?

从概念态到定义态:

(1)实例化一个ApplicationContext的对象;

(2)调用bean工厂后置处理器(invokeBeanFactoryPostProcess)完成扫描;

(3)循环解析扫描出来的类信息;

(4)实例化一个BeanDefinition对象来存储解析出来的信息;

(5)把实例化好的beanDefinition对象put到beanDefinitionMap当中缓存起来,以便后面实例化bean;

(6)再次调用其他bean工厂后置处理器;

从定义态到纯净态:

(7)当然Spring还会干很多事情,比如国际化,比如注册BeanPostProcessor等等,如果我们只关心如何实例化一个bean的话那么这一步就是Spring调用finishBeanFactoryInitialization方法来实例化单例的bean,实例化之前spring要做验证,需要遍历所有扫描出来的类,依次判断这个bean是否lazy,是否prototype,是否abstract等等;

必须满足:不是懒加载、是单例、是抽象才会被生产这个bean。

(8)如果验证完成spring在实例化一个bean之前需要推断构造方法,因为spring实例化对象是通过构造方法反射,故而需要知道用哪个构造方法;

(9)推断完构造方法之后spring调用构造方法反射实例化一个对象,注意这里说的是对象,这个时候对象已经实例化出来了,但是并不是一个完整的bean,最简单的体现是这个时候实例化出来的对象属性是没有注入,所以不是一个完整的bean。

纯净态到成熟态:

(10)spring处理合并后的beanDefinition;

(11)判断是否需要完成属性注入

(12)如果需要完成属性注入,则开始注入属性;

初始化

(13)判断bean的类型回调Aware接口;

(14)调用生命周期回调方法;

(15)如果需要代理则完成代理;

(16)put到单例池——bean完成——存在spring容器当中。

21、Spring IOC有哪些扩展点?在什么时候调用?

(1)执行BeanFactoryPostProcessor的postProcessBeanFactory方法。

1
2
3
4
5
6
7
8
9
/***
* 作用:在注册BeanDefinition,可以对beanFactory进行扩展
* 调用时机:IOC加载时注册BeanDefinition的时候会调用。
*/
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor{
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeandException{
}
}

(2)执行BeanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry方法

22、注解的实现原理?

注解的作用目标(类、方法、字段的属性)、注解的生命周期(SOURCE源代码、CLASS类加载、RUNTIME运行时)。当我们把一个注解加到目标上时,可以通过getAnnotation等方法以反射的形式代理这个接口。

实际上,Java注解的实现依赖于反射(Reflection)机制。反射机制允许程序在运行时获取类的信息,如类的成员、方法、注解等。通过反射,你可以在运行时获取并解析注解信息。

对于一个注解,Java会在编译时将其转化为一个接口,接口名字就是注解的名字。注解的成员变量会被转化为接口的方法。这个接口会继承自Annotation接口。

23、Spring的事务是怎么管理的?

(1)声明式事务管理: 通过配置文件或注解来定义事务的范围和属性,将事务管理与业务逻辑分离,使代码更加清晰。

(2)编程式事务管理: 在代码中显式地控制事务的开始、提交和回滚,给开发者更大的灵活性。

24、MyBatis自定义封装一个查询怎么做?

在MyBatis中,你可以通过自定义一个Mapper接口来封装自定义查询。

(1)首先,需要创建一个Mapper接口,用于定义自定义查询方法。这个接口会对应一个XML文件,用于实现具体的SQL查询逻辑。

(2)在MyBatis的XML配置文件夹中创建一个与Mapper接口对应的XML文件,在这个XML文件中编写SQL查询逻辑。

(3)在MyBatis的配置文件(通常是mybatis-config.xml)中注册这个Mapper接口。

(4)在需要使用自定义查询的地方,通过注入SqlSession对象来调用自定义Mapper接口的方法。

25、Servlet如何自定义管理一个事务?

在Servlet中,可以通过使用javax.servlet.Filter接口来自定义管理一个事务。Filter可以在Servlet处理请求之前和之后执行一些逻辑,可以在Filter中开启、提交或回滚事务。

SpringBoot系列

1、SpringBoot是如何启动Tomcat的?

(1)首先,SpringBoot在启动时会先创建一个Spring容器;

(2)在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean;

(3)Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat。

2、SpringBoot中常用注解及其底层实现?

(1)@SpringBootApplication注解:这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:

​ a. @SpringBootConfiguration:这个注解实际就是一个@Configuration,表示启动类也是一个配置类;

​ b. @EnableAutoConfiguration:向Spring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean;

​ c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录。

(2)@Bean注解:用来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象。

(3)@Controller、@Service、@ResponseBody、@Autowired。

SpringCloud系列

1、SpringCloud和Dubbo有哪些区别?

SpringCloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo一开始是一个RPC调用框架,核心是解决服务调用间的问题,SpringCloud是一个大而全的框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有SpringCloud全面,但是Dubbo的服务调用性能比SpringCloud高,不过SpringCloud和Dubbo并不是对立的,是可以结合起来一起使用的。

2、SpringCloud有哪些常用组件,作用是什么?

(1)Eureka:注册中心;

(2)Nacos:注册中心、配置中心;

(3)Consul:注册中心、配置中心;

(4)Spring Cloud Config:配置中心;

(5)Feign/OpenFeign:RPC调用;

(6)Kong:服务网关;

(7)Zuul:服务网关;

(8)Spring Cloud Gateway:服务网关;

(9)Ribbon:负载均衡;

(10)Spring Cloud Sleuth:链路追踪;

(11)Zipkin:链路追踪;

(12)Seata:分布式事务;

(13)Dubbo:RPC调用;

(14)Sentinel:服务熔断;

(15)Hystrix:服务熔断。

微服务

1、SOA、分布式、微服务之间有什么关系和区别?

(1)SOA是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用。

(2)微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应一个服务的架构。

(3)分布式架构是指将单体架构中的各个部分拆分,然后部署到不同的机器或进程中去,SOA和微服务基本上都是分布式架构的。

2、RocketMQ的事务消息是如何实现的?

(1)生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的;

(2)再创建订单,根据创建订单成功与否,向Broker发送commit或rollback;

(3)并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功;

(4)一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束;

(5)如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理。

3、Nacos是做什么的?

Nacos是一个开源的动态服务发现、配置管理和服务管理平台。主要功能包括:

(1)动态服务发现:Nacos可以自动检测新加入或者退出的服务实例,保证服务 实例的动态变更能够及时地被发现。

(2)动态配置管理:Nacos允许以集中化、动态的方式管理微服务的配置信息,可以随时修改配置并实时生效。

(3)服务健康监测:Nacos能够周期性地检测注册的服务实例的健康状态,并提供了相应的API和控制台界面用于监控服务健康状况。

(4)服务和实例管理:Nacos提供了控制台界面,可以用于管理注册的服务以及它们的实例信息。

Zookeeper

1、Zookeeper和Eureka的区别?

zk:CP设计(强一致性),目标是一个分布式的协调系统,用于进行资源的统一管理。

当节点crash后,需要进行leader的选举,在这个期间内,zk服务是不可用的。

eureka:AP设计(高可用),目标是一个服务注册发现系统,专门用于微服务的服务发现注册。

Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时如果发现连接失败,会自动切换至其他节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。

同时,当eureka的服务端发现85%以上的服务都没有心跳的话,它就会认为自己的网络出了问题,就不会从服务列表中删除这些失去心跳的服务,同时eureka的客户端也会缓存服务信息。eureka对于服务注册发现来说是非常好的选择。

2、Zookeeper集群中节点之间数据是如何同步的?

(1)首先集群启动时,会先进行领导者选举,确定哪个节点是Leader,哪些节点是Follower和Observer。

(2)然后Leader会和其他节点进行数据同步,采用发送快照和发送Diff日志的方式。

(3)集群在工作过程中,所有的写请求都会交给Leader节点来进行处理,从节点只能处理读请求。

(4)Leader节点收到一个写请求时,会通过两阶段机制来处理。

(5)Leader节点会将该写请求对应的日志发送给其他Follower节点,并等待Follower节点持久化日志成功。

(6)Follower节点收到日志后会进行持久化,如果持久化成功则发送一个ACK给Leader节点。

(7)当Leader节点收到半数以上的ACK后,就会开始提交,先更新Leader节点本地的内存数据。

(8)然后发送commit命令给Follower节点,Follower节点收到commit命令后就会更新各自本地内存数据。

(9)同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执行更新本地内存数据。

(10)最后Leader节点返回客户端写请求响应成功。

(11)通过同步机制和两阶段提交机制来达到集群中节点数据一致。

3、Zookeeper中的领导者选举的流程是怎样的?

(1)集群中 各个节点首先都是观望状态(LOOKING),一开始都会投票给自己,认为自己比较适合作为leader。

(2)然后相互交互投票,每个节点都会收到其他节点发过来的选票,然后pk,先比较zxid,zxid大者获胜,zxid如果相等则比较myid,myid大者获胜。

(3)一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更大的节点,并将选票放入自己的投票箱中,并将新的选票发送给其他节点。

(4)如果PK是平局则将接收到的选票放入自己的投票箱中。

(5)如果PK赢了,则忽略所接收到的选票。

(6)当然一个节点将一张选票放入到自己的投票箱之后,就会从投票箱中统计票数,看是否超过一半的节点都和自己所投的节点是一样的,如果超过半数,那么则认为当前自己所投的节点是leader。

(7)集群中每个节点都会经过同样的流程,PK的规则也是一样的,一旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的leader是一样的,这样集群的leader就选举出来了。

JVM

一、JVM篇

1、说说JDK、JRE、JVM?

JDK,Java标准开发包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境、以及常用的Java类库等。

JRE,Java运行环境,用于运行Java的字节码文件。JRE中包括了JVM以及JVM工作所需的类库,而普通用户只需要安装JRE来运行Java程序,而程序开发者必须安装JDK来编译、调试程序。

JVM,Java虚拟机,是JRE的一部分,它是整个java实现跨平台的最核心部分,负责运行字节码文件。

JDK中包含了JRE,JRE中包含了JVM。

2、JVM内存模型?

(1)程序计数器:用于存储当前线程执行的字节码指令的地址。在多线程环境下,每个线程都有自己独立的程序计数器,以保证线程切换后能恢复到正确的执行位置。

(2)Java虚拟机栈: 每个线程都有一个私有的Java虚拟机栈,用于存储方法调用的局部变量、操作数栈、动态链接、方法出口等信息。每个方法的调用都会创建一个栈帧,方法执行完毕后栈帧会被销毁。如果线程请求的栈深度大于虚拟机允许的深度,将抛出栈溢出异常。

(3)本地方法栈: 与虚拟机栈类似,用于支持Native方法的执行。

(4)堆:用于存储对象实例和数组。

(5)方法区: 存储类的元数据、常量等信息。运行时常量池是方法区的一部分,用于存放编译时生成的各种字面量和符号引用。在类加载后,常量池会被加载到内存中。

(6)本地内存: 直接内存不是JVM内部的一部分,但也被广泛使用。它是一种在Java堆之外直接分配内存的方式,通常与NIO相关。直接内存的分配和释放需要通过本地方法库来实现。

3、Java虚拟机中有哪些类加载器?

(1)启动类加载器:这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。

(2)扩展类加载器:这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

(3)应用程序类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

(4)自定义类加载器:用户自定义的类加载器。

4、类加载的过程?

类加载的过程包括:加载、验证、准备、解析、初始化,其中验证、准备、解析统称为连接。

(1)加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象。

(2)验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

(3)准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值“通常情况”下是数据类型的零值。

(4)解析:将常量池内的符号引用替换为直接引用。

(5)初始化:到了初始化阶段,才真正开始执行类中定义的Java初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。

5、JVM有哪些垃圾回收算法?

(1)标记清除算法:

标记阶段:把垃圾内存标记出来;

清除阶段:直接将垃圾内存回收;

这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。

(2)复制算法:为了解决标记清除算法的内存碎片问题,复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。

(3)标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。

6、JVM垃圾回收机制?

在触发GC的时候,会使用垃圾回收机制。

对那些JVM认为已经“死掉”的对象进行垃圾收集,新生代使用复制算法,老年代使用标记-清除和标记-整理算法。

7、GC Roots有哪些?

可作为GC Roots的对象包括下面几种:

(1)虚拟机栈中引用的对象。

(2)方法区中类静态属性引用的对象。

(3)方法区中常量引用的对象。

(4)本地方法栈中JNI(即一般说的Native方法)引用的对象。

8、什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

9、使用双亲委派模型的好处?

使用双亲委派模型来组织类加载器之间的关系,好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

10、怎么判定对象已经“死去”?

常见的判定方法有两种:引用计数法和可达性分析算法,HotSpot中采用的是可达性分析算法。

(1)引用计数法:

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

客观地说,引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,但是主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

(2)可达性分析算法:
就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

11、HotSpot为什么要分为新生代和老年代?

HotSpot根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

其中新生代又分为1个Eden区和2个Survivor区,通常称为From Survivor和To Survivor区。

img

12、HotSpot GC的分类?

(1)Partial GC:并不收集整个GC堆的模式,具体如下:

​ 1)Young GC/Minor GC:只收集新生代的GC。

​ 2)Old GC:只收集老年代的GC。只有CMS的concurrent collection是这个模式。

​ 3)Mixed GC:收集整个新生代以及部分老年代的GC,只有G1有这个模式。

(2)Full GC/Major GC:收集整个GC堆的模式,包括新生代、老年代、永久代等所有部分的模式。

13、HotSpot GC的触发条件?

(1)触发Young GC:当新生代中的Eden区没有足够空间进行分配时会触发Young GC。

(2)触发Full GC:

​ 1)当准备要触发一次Young GC时,如果发现统计数据说之前Young GC的平均晋升大小比目前老年代剩余的空间大,则不会触发Young GC而是转为触发Full GC。
​ 2)如果有永久代的话,在永久代需要分配空间但已经没有足够空间时,也要触发一次Full GC。
​ 3)System.gc()默认也是触发Full GC。
​ 4)heap dump带GC默认也是触发Full GC。

​ 5)CMS GC时出现Concurrent Mode Failure会导致一次Full GC的产生。

14、Full GC后老年代的空间反而变小?

HotSpot的Full GC实现中,默认新生代里所有活的对象都要晋升到老年代,实在晋升不了才会留在新生代。假如做Full GC的时候,老年代里的对象几乎没有死掉的,而新生代又要晋升活对象上来,那么Full GC结束后老年代的使用量自然就上升了。

15、什么情况下新生代对象会晋升到老年代?

(1)如果新生代的垃圾收集器为Serial和ParNew,并且设置了-XX:PretenureSizeThreshold参数,当对象大于这个参数值时,会被认为是大对象,直接进入老年代。

(2)Young GC后,如果对象太大无法进入Survivor区,则会通过分配担保机制进入老年代。

(3)对象每在Survivor区中“熬过”一次Young GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,可以通过-XX:MaxTenuringThreshold设置),就将会被晋升到老年代中。

(4)如果在Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

16、发生Young GC的时候需要扫描老年代的对象吗?

在分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,如果回收新生代时,不得不同时扫描老年代的话,那么Young GC的效率可能下降不少。

17、垃圾收集器有哪些?

(1)Serial。Serial是一个单线程的收集器,它不但只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。适合用于客户端垃圾收集器。

(2)Parnew。ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。

(3)parallel Scavenge。Parallel Scavenge收集器关注点是吞吐量。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间;高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。

(4)Serial old。Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。主要有两个用途: 1)在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。 2)作为年老代中使用CMS收集器的后备垃圾收集方案。

(5)JDK8-CMS:(关注最短垃圾回收停顿时间)。CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。初始标记、并发标记、重新标记、并发清除。

(6)JDK9-G1:(精准控制停顿时间,避免垃圾碎片)。是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以及高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。相比与CMS收集器,G1收集器两个最突出的改进是: 1)基于标记-整理算法,不产生内存碎片。 2)可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

18、如何配置垃圾收集器?

(1)首先是内存大小问题,基本上每一个内存区域都会设置一个上限,来避免溢出问题,比如元空间。

(2)通常,堆空间会设置成操作系统的2/3,超过8GB的堆,优先选用G1。

(3) 然后采用JVM进行初步优化,比如根据老年代的对象提升速度,来调整年轻代和老年代之间的比例。

(4)依据系统容量、访问延迟、吞吐量等进行专项优化,服务是高并发的,对STW的时间敏感。

(5)会通过记录详细的GC日志,来找到这个瓶颈点,借用GCeasy这样的日志分析工具,定位问题。

19、JVM性能调优?

常用命令:jps、jinfo、jstat、jstack、jmap。

image-20230831135800015

image-20230831135807923

image-20230831135826774

image-20230831135843675

image-20230831135851939

20、内存溢出问题?

(1)当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生内存溢出问题。

(2)假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误。

(3)内存申请过大:程序试图申请超过可用内存的空间,超出了操作系统或者硬件的控制。

(4)内存泄漏:程序中存在未被释放的内存块,随着程序的执行,这些未释放的内存会逐渐累积,最终导致内存耗尽。

(5)同时运行多个大内存消耗程序:如果同时运行了多个需要大量内存的程序,可能会导致系统内存不足。

(6)内存碎片问题:即使总内存足够,但是由于内存碎片的存在,导致没有足够的连续内存块来分配给程序。

解决方法:

(1)优化程序设计,减少内存消耗。

(2)检查是否存在内存泄漏,并修复。

(3)使用合适的数据结构和算法,避免不必要的内存占用。

(4)合理配置虚拟内存。

(5)确保及时释放不再使用的内存块。

(6)分析系统资源使用情况,避免过度占用资源。

Java 基础

面试经验

一、大数据

1、Hadoop的核心组件?

(1)HDFS:分布式文件系统,用于在集群中存储大规模数据。它将数据分割成多个块,然后分布在集群的各个节点上,以实现数据冗余和容错性。HDFS适用于批量数据读写操作,但并不适合低延迟的随机读写操作。

(2)Yarn:Hadoop的资源管理器,负责集群中的资源分配和作业调度,它将集群的计算资源划分为多个容器,然后将这些容器分配给不同的任务。

(3)MapReduce:是Hadoop的计算模型,用于分布式处理数据。它将大规模的任务划分成多个小的任务,然后在集群的各个节点上并行执行。主要由两个阶段组成:Map阶段负责将输入数据映射成键值对,然后Reduce阶段将相同键的值进行聚合和处理。

MapReduce适用于批处理的数据处理任务,如数据清洗、转换和分析。

2、Spark是用来做什么的?

Spark是一个用于大规模数据处理的开源分布式计算框架,旨在处理大规模数据处理任务,如批处理(类似于MapReduce)、交互式查询(Spark SQL)、机器学习(MLlib)和流处理(Spark Streaming)。与Hadoop的MapReduce不同,它提供了更高级别的API和内存计算能力,可以加速数据处理和分析任务。

特点:

(1)Spark支持弹性分布式数据集(RDD)作为其核心数据抽象。RDD是一个可分区的、可并行操作的、容错的数据集合,它可以在内存中缓存数据,从而加速数据处理。

(2)Spark提供了多种编程接口,包括Scala、Java、Python和R。其中,Scala接口是最原生和性能最好的。

(3)Spark将中间数据存储在内存中,从而避免了在磁盘上频繁读写数据,提高了计算速度。这使得Spark在迭代计算、交互式查询等场景中比传统的MapReduce更快。

(4)Spark具备容错性,它可以通过重新计算丢失的数据来恢复节点故障,从而保证数据处理的可靠性。

3、Hbase是什么?优点?

HBase是一个分布式、面向列的数据库,适用于存储大量结构化数据,并且可以实现实时读写访问。

(1)快速读写:Hbase采用了面向列的存储结构和内存缓存,使数据的读写操作可以在毫秒级完成。

(2)强大的数据模型:Hbase的数据模型类似于一个多维的、稀疏的分布式映射表。这使得可以根据行键、列族和列限定符来高效地存储和检索数据。

(3)灵活的数据架构:Hbase的数据模型可以适应不同类型的数据,包括结构化数据(可用表格和行列表示)、半结构化数据(xml、json)和非结构化数据(文本、图像、音频和视频)。

(4)实时查询:Hbase支持随机访问,允许通过行键进行高效的单行或多行查询。这对于需要快速获取特定数据的实时应用非常有用。

(5)高扩展性:Hbase能够水平扩展,使用不断增长的数据量。通过在集群中添加更多的节点,可以实现更大规模的数据存储和处理。

(6)Hbase支持强一致性和弱一致性,允许根据应用需求选择适当的数据一致性级别。

4、Hbase和普通数据库的区别?

(1)读写性能:Hbase采用内存缓存和面向列的存储结构,使得其在实时读写操作方面具有优势,可以快速地进行随机访问。而关系型数据库的性能通常受限于磁盘读写速度,对于大规模数据的实时查询可能存在一定的性能挑战。

(2)数据一致性:Hbase提供了强一致性和弱一致性,可以根据应用需求选择不同的数据一致性级别。而关系型数据库通常采用ACID事务模型,保证数据的强一致性。

(3)数据模型:Hbase采用了面向列的数据模型,数据存储在分布式表格中,每个表格可以有多个列族,每个列族下可以有多个列限定符,这种模式适合存储稀疏、非结构化或半结构化的数据。

而关系型数据库采用表格形式存储数据,每个表格有固定的列,行中存储的是具体的数据项,这种模式适合存储结构化数据。

(4)适用场景:Hbase适用于存储大量结构化、半结构化和非结构化的数据,特别是需要实时读写访问的场景,如日志存储、实时监控等。

关系型数据库适用于存储事务性数据,如订单、用户信息等,以及需要强一致性和负责查询的场景。

(5)表结构变更:Hbase的列族和列限定符可以动态添加,不需要预定义表结构。

而关系型数据库适用于存储事务性数据,如订单、用户信息等,以及需要强一致性和复杂查询的场景。

5、Zookeeper的作用?

Zookeeper是一个开源的分布式协调服务。可以用于

(1)实现分布式锁:确保在分布式系统中只有一个节点能够访问共享资源。

(2)配置管理:可以将配置信息存储在Zookeeper中,各个节点都可以监听这些配置信息的变化,从而动态地更新配置。

(3)提供命名服务,用于注册和发现各个节点的信息。

(4)实现分布式队列,用于协调多个节点之间的任务执行顺序。

6、采用大数据编程的优点?

(1)处理大规模数据、并行处理:比如说在大数据编程框架Hadoop中,在MapReduce任务执行之前,数据首先被加载到HDFS中。然后,MapReduce作业将在集群中的不同节点上并行执行,这些节点可以访问存储在HDFS中的数据块。Map阶段将数据块分割成键值对,并对每个键值对应用用户定义的映射函数。Reduce阶段将具有相同键的值进行聚合和处理,最终生成最终的结果。这种数据加载和处理方式使得MapReduce能够在分布式集群上高效地处理大规模数据集,从而实现数据的并行计算和分布式处理。

(2)实时处理:比如像Spark Streaming、Flink这样的大数据编程框架支持实时数据流处理,可以帮助组织实时监控、实时分析和实时决策。

7、分布式是什么意思?

分布式就是指将计算任务或数据处理任务分散到多台计算机或节点上进行执行的一种计算方式。在分布式系统中,各个计算机节点之间通过网络通信协作,共同完成整体的任务或处理工作。可以提高计算能力、可扩展性、容错性以及资源利用率。

8、前后端如何通信?

(1)AJAX:使用XMLHTTPREQUEST技术构建更复杂、动态的网页编程实践。前端通过发送请求给后端,后端则返回相应的数据。

(2)WebSocket:WebSocket是一种基于TCP协议的双向通信协议,它允许客户端和服务器之间进行实时通信。

(3)RESTful API:前后端通过HTTP协议进行通信,前端通过发送请求给后端,后端则返回相应的数据。RESTful API是目前最常用的前后端交互方式之一。

9、vue的生命周期?

(1)beforeCreate(在实例初始化之后,数据观测和event/watcher事件配置之前被调用)。

(2)created(实例已经创建完成之后被调用)。

(3)beforeMount(在挂载开始之前被调用:render函数首次被调用)。

(4)mounted(el被新创建的vm.$el替换,并挂载在实例上之后调用该钩子)。

(5)beforeUpdate(数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前)。

(6)updated(由于数据更改导致的虚拟DOM重新渲染和打补丁后调用)。

(7)beforeDestroy(实例销毁之前调用)。

(8)destroyed(实例销毁后调用)。

10、面向过程与面向对象的区别?

(1)面向过程编程侧重于解决问题的步骤和过程,以一系列的函数或者过程为基础,通过控制流程来达到解决问题的目的。而面向对象编程侧重于将数据和对数据的操作封装在一个对象中,通过对象之间的交互来解决问题。

(2)面向过程中数据和对数据的操作是分开的,函数对数据的操作是通过参数传递的。面向对象中数据和对数据的操作被组织成一个对象,对象内部包含了数据和处理数据的方法。

(3)面向过程的可重用性较低,因为函数是相对独立的,难以复用于其他项目。面向对象的可重用性相对较高,因为对象可以在不同的上下文中重复使用。

(4)面向过程主要以函数为单位,函数是对一系列操作的封装。而面向对象是以类和对象为单位,类定义了对象的属性和方法。

二、Java基础篇

0、面向对象的三大特性?

(1)封装:隐藏部分对象的属性和实现细节,对数据的访问只能通过对外公开的接口。通过这种方式,对象内部数据提供了不同级别的保护,以防止程序中无关部分意外的改变,或错误的使用了对象的私有部分。

(2)继承:让某个类型的对象获得另一个类型对象的属性和方法。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

1、多态的实现原理?

多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。

静态绑定与动态绑定:

一种是在编译期确定,被称为静态分派,比如方法的重载;

一种是在运行时确定,被称为动态分派,比如方法的重写和接口的实现。

多态的实现:

多态的实现过程,就是方法调用动态分派的过程,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。

2、静态变量和成员变量的区别?

(1)成员变量存放于堆内存中。静态变量存在于方法区中。

(2)成员变量与对象共存亡,随着对象创建而存在,随着对象被回收而释放。静态变量与类共存亡,随着类的加载而存在,随着类的消失而消失。

(3)成员变量所属于对象,所以也称为实例变量。静态变量所属于类,所以也称为类变量。

(4)成员变量只能被对象所调用。静态变量可以被对象调用,也可以被类名调用。

3、是否可以从一个静态方法内部发出对非静态方法的调用?

分为两种情况,发出调用时是否显示创建了对象实例。

(1)没有显示创建对象实例:不可以发起调用,非静态方法只能被对象所调用,静态方法可以通过对象调用,也可以通过类名调用,所以静态方法被调用时,可能还没有创建任何实例对象。因此通过静态方法内部发出对非静态方法的调用,此时可能无法知道非静态方法属于哪个对象。

(2)显示创建对象实例:可以发起调用,在静态方法中显示的创建对象实例,则可以正常的调用。

4、==和equals的区别是什么?

==:运算符,用于比较基础类型变量和引用类型变量。对于基础类型变量,比较变量保存的值是否相同,类型不一定要相同。对于引用类型变量,比较的是两个对象的地址是否相同。

equals:Object类中定义的方法,通常用于比较两个对象的值是否相等。

equals在Object方法中其实等同于==,但是在实际的使用中,equals通常被重写用于比较两个对象的值是否相同。

5、深拷贝和浅拷贝区别是什么?

深拷贝:对于引用数据类型,开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。

浅拷贝:对于引用数据类型,只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。

深拷贝相比于浅拷贝速度较慢并且花销较大。

数据分为基本数据类型和引用数据类型。基本数据类型:数据直接存储在栈中。引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。

6、HashMap底层,为什么要使用红黑树?

底层是由“数组+链表+红黑树”组成。在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。

为了加快检索速率。红黑树虽然本质上是一颗二叉树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。

7、在什么时候使用链表?什么时候使用红黑树?

对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后超过8个(阈值8),如果此时数组长度大于等于64,则会触发链表节点转红黑树节点,而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。

对于移除,当同一个索引位置的节点在移除后达到6个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。

8、HashMap的默认初始容量?HashMap的容量有什么限制?

默认初始容量是16。HashMap的容量必须是2的N次方,HashMap会根据传入的容量计算一个大于等于该容量的最小的2的N次方,例如传9,容量为16。

9、HashMap的插入流程是怎么样的?

(1)首先,通过键的hashCode()方法计算出键的哈希值。哈希值是一个整数,用于确定该键值对在哈希表中的位置。

(2)确定存储位置。使用哈希值和哈希表的容量(数组的长度)来计算出该键值对在数组中的索引位置。通常会使用 (hash & (capacity - 1)) 的位运算来计算位置,其中 hash 是键的哈希值,capacity 是数组的长度。

(3)处理哈希冲突。如果发生了哈希冲突(即不同的键计算出了相同的位置),则会使用链表或红黑树来解决冲突。在JDK 8及之前的版本中,使用链表来存储具有相同哈希值的键值对,称为链地址法。从JDK 8开始,当链表的长度超过一定阈值(默认为8),链表会转化为红黑树,以提高查找效率。

(4)插入键值对。如果该位置没有元素,直接将键值对插入到该位置。如果该位置已经存在元素,会根据键的哈希值进行比较:如果哈希值相等且键相等(调用 equals() 方法返回 true),则更新对应的值。否则,将该键值对插入到链表或红黑树的末尾。

10、HashMap的扩容流程是怎么样的?

(1)当HashMap中的元素数量达到容量乘以加载因子时,就会触发扩容操作。加载因子默认为0.75,可以通过构造方法设置。

(2)当需要扩容时,HashMap 会创建一个新的数组,其大小为原数组的两倍。

(3)对于原数组中的每个非空位置(即存储了键值对的位置),会重新计算它们在新数组中的位置。

重新计算的方法是通过对原位置的哈希值取模新数组长度。

(4)对于每个原数组的位置,如果它的链表长度小于等于阈值(默认为8),则直接将链表中的键值对按照顺序放入新数组的相应位置。如果链表长度超过阈值,会将该链表转化为红黑树,然后再将红黑树的节点按照顺序放入新数组的相应位置。

(5)当新数组中的所有元素都被放入新的位置后,新数组取代了原来的数组,成为新的存储结构。

(6)扩容操作完成后,原数组会被垃圾回收。

11、除了HashMap,还用过哪些Map,在使用时怎么选择?

img

12、HashMap和Hashtable的区别?

(1)HashMap允许key和value为null,Hashtable不允许。

(2)HashMap的默认初始容量为16,Hashtable 为11。

(3)HashMap的扩容为原来的2倍,Hashtable的扩容为原来的2倍加1。

(4)HashMap是非线程安全的,Hashtable是线程安全的。

(5)HashMap的hash值重新计算过,Hashtable直接使用hashCode。

(6)HashMap去掉了Hashtable中的contains方法。

(7)HashMap继承自AbstractMap类,Hashtable继承自Dictionary类。

13、hashcode是干什么用的?可以不重写hashcode方法吗?会出现什么问题?

hashcode是Java中的一个方法,它用于计算对象的哈希码。在 Java中,hashcode方法被用于哈希集合(如 HashSet)、哈希映射(如 HashMap)等数据结构中,以及在对象比较中用于提高性能。

如果不重写hashcode方法,而只重写了equals方法,可能会导致以下问题:

(1)在哈希集合中无法准确查找: 如果对象的equals方法判断为相等,但哈希码不同,那么在哈希集合中查找对象时可能无法找到对应的项。

(2)哈希映射中的问题: 在哈希映射中,相同的键应该具有相同的哈希码,否则可能导致无法正确地查找或删除映射项。

(3)不稳定的哈希集合和映射行为: 如果对象被修改后导致哈希码发生变化,它可能无法正确地从哈希集合或映射中移除或查找。

14、Java中的初始化?

(1)在类的内部,变量定义的先后顺序决定了初始化顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造方法)被调用之前得到初始化。
(2)无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初始化值就是null。
(3)静态初始化只有在必要时刻才进行,例如:类里面的静态变量,只有当类被调用时才会初始化,并且静态变量不会再次被初始化,即静态变量只会初始化一次。
(4)初始化的顺序是先静态对象,然后是非静态对象。
(5)当有父类时,完整的初始化顺序为:父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器 ->子类非静态变量(非静态代码块)->子类构造器 。
(6)即使没有显示的使用static关键字,构造器实际上也是静态方法。

15、String、StringBuilder、StringBuffer的区别?

String:String的值被创建后不能修改,任何对String的修改都会引发新的String对象的生成。

StringBuffer:跟String类似,但是值可以被修改,使用synchronized来保证线程安全。

StringBuilder:StringBuilder的非线程安全版本,没有使用synchronized,具有更高的性能,推荐优先使用。

16、什么是反射?

反射是指在运行状态中,对于任意一个类都能够知道这个类的所有的属性和方法,并且对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能称为反射机制。

17、重载和重写的区别?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载:一个类中有多个同名的方法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。

重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。

18、为什么不能根据返回类型来区分重载?

方法的返回值只是作为方法运行之后的一个“状态”,但是并不是所有调用都关注返回值,所以不能将返回值作为重载的唯一区分条件。

19、抽象类和接口有什么区别?

抽象类只能单继承,接口可以多实现。

抽象类可以有构造方法,接口中不能有构造方法。

抽象类中可以有成员变量,接口中没有成员变量,只能有常量(默认就是 public static final)。

抽象类中可以包含非抽象的方法,在Java 7之前接口中的所有方法都是抽象的,在Java 8之后,接口支持非抽象方法:default 方法、静态方法等。Java 9支持私有方法、私有静态方法。

抽象类中的方法类型可以是任意修饰符,Java 8之前接口中的方法只能是public 类型,Java 9支持private类型。

设计思想的区别:

接口是自上而下的抽象过程,接口规范了某些行为,是对某一行为的抽象。我需要这个行为,我就去实现某个接口,但是具体这个行为怎么实现,完全由自己决定。

抽象类是自下而上的抽象过程,抽象类提供了通用实现,是对某一类事物的抽象。我们在写实现类的时候,发现某些实现类具有几乎相同的实现,因此我们将这些相同的实现抽取出来成为抽象类,然后如果有一些差异点,则可以提供抽象方法来支持自定义实现。

20、Error和Exception有什么区别?

Error和Exception都是Throwable的子类,用于表示程序出现了不正常的情况。区别在于:

(1)Error表示系统级的错误和程序不必处理的异常,是恢复不可能但很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。

(2)Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况。

21、Java中的final关键字有哪些用法?

(1)修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract和final。

(2)修饰方法:该方法不能被子类重写。

(3)修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。 如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。

22、阐述final、finally、finalize的区别。

finally是对Java异常处理机制的最佳补充,通常配合try、catch使用,用于存放那些无论是否出现异常都一定会执行的代码。在实际使用中,通常用于释放锁、数据库连接等资源,把资源释放方法放到finally中,可以大大降低程序出错的几率。

finalize:Object中的方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。在Java 9中该方法已经被标记为废弃,并添加了新的java.lang.ref.Cleaner,提供了更灵活和有效的方法来释放资源。

23、JDK1.8之后有哪些新特性?

(1)接口默认方法:Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可。

(2)Lambda表达式和函数式接口:Lambda表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。Lambda允许把函数作为一个方法的参数,使用 Lambda 表达式使代码更加简洁,但是也不要滥用,否则会有可读性等问题。

(3)方法引用:可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

(4)日期时间API:Java 8 引入了新的日期时间API改进了日期时间的管理。

(5)Stream API:用函数式编程方式在集合类上进行复杂操作的工具,配合Lambda表达式可以方便的对集合进行处理。使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询。也可以使用Stream API来并行执行操作。

24、为什么Java中只有值传递?

值传递:在调用函数时,将实参拷贝一份赋值给函数的形参,对形参进行操作;

引用传递:在函数调用时,将实参传递给函数,直接对实参进行操作。

Java方法传参,都是对所传变量进行拷贝,对基本数据类型来讲,拷贝的是实际数值,对引用数据类型来讲拷贝的是引用地址。

Java中不存在函数对实参的操作,全部是对经过拷贝的形参的操作,也就是说Java中只存在值传递,不存在引用传递。

25、List、Set、Map三者的区别?

(1)List: List 接口存储一组不唯一(可以有多个元素引用相同的对象)、有序的对象。

(2)Set:不允许重复的集合,不会有多个元素引用相同的对象。

(3)Map:使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

26、ArrayList和LinkedList的区别?

ArrayList底层基于动态数组实现,LinkedList底层基于链表实现。

对于按index索引数据(get/set方法):ArrayList通过index直接定位到数组对应位置的节点,而LinkedList需要从头结点或尾节点开始遍历,直到寻找到目标节点,因此在效率上ArrayList优于LinkedList。

对于随机插入和删除:ArrayList需要移动目标节点后面的节点(使用System.arraycopy方法移动节点),而LinkedList只需修改目标节点前后节点的 next或prev属性即可,因此在效率上LinkedList优于ArrayList。

对于顺序插入和删除:由于ArrayList不需要移动节点,因此在效率上比LinkedList 更好。

27、ArrayList和Vector的区别?

Vector和ArrayList几乎一致,唯一的区别是Vector在方法上使用了synchronized来保证线程安全,因此在性能上ArrayList具有更好的表现。

28、ConcurrentHashMap?

一种线程安全的高效Map集合,它是【线程安全】的哈希表。它是通过“分段锁”来实现多线程下的安全问题。

  • 底层数据结构

    • jdk1.7采用分段数组+链表实现
    • jdk1.8采用数组+链表/红黑树
  • 加锁的方式

    • jdk1.7采用segment分段锁,底层使用的是ReentrantLock
    • Jdk1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点。

29、Volatile和Synchronized的区别?

volatile关键字详解:是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

(1)作用的位置不同。
synchronized是修饰方法,代码块。volatile是修饰变量。

(2)作用不同。
synchronized,可以保证变量修改的可见性及原子性,可能会造成线程的阻塞;synchronized在锁释放的时候会将数据写入主内存,保证可见性。
volatile仅能实现变量修改的可见性,但无法保证原子性,不会造成线程的阻塞;volatile修饰变量后,每次读取都是去主内存进行读取,保证可见性。

30、设计模式?

(1)单例模式

某个类只能生成一个实例,该实例全局访问,例如Spring容器里一级缓存里的单例池。

优点: (1)唯一访问:如生成唯一序列化的场景、或者spring默认的bean类型。(1)提高性能:频繁实例化创建销毁或者耗时耗资源的场景,如连接池、线程池。

缺点: 不适合有状态且需变更的。

实现方式:

(1)饿汉式:线程安全速度快 。

(2)懒汉式:双重检测锁,第一次减少锁的开销、第二次防止重复、volatile防止重排序导致实例化未完成 。

(3)静态内部类:线程安全利用率高 。

(4)枚举:effictive JAVA推荐,反射也无法破坏。

(2)工厂模式

定义一个用于创建产品的接口,由子类决定生产何种产品。

优点:解耦,提供参数即可获取产品,通过配置文件可以不修改代码增加具体产品。

缺点:每增加一个产品就得新增一个产品类。

(3)抽象工厂模式

提供一个接口,用于创建相关或者依赖对象的家族,并由此进行约束。

优点:可以在类的内部对产品族进行约束。

缺点:假如产品族中需要增加一个新的产品,则几乎所有的工厂类都需要进行修改。

(4)原型模式

将一个对象作为原型,采用clone()方法来创建新的实例。

31、JUC知道哪些?

JUC是Java标准库中的一个包。

(1)ReentrantLock可重入锁。

(2)Executors,提供了一些常用的线程池工厂方法。

(3)ThreadPoolExecutor,线程池的实现类。

(4)ConcurrentHashMap,线程安全的哈希表实现。

(5)CountDownLatch,一个同步辅助类,用于等待一组线程完成后再执行。

32、Java的集合有哪些?

ArrayList、LinkedList、HashMap、TreeMap、HashTable、Vector、HashSet、LinkedHashSet、TreeSet、Queue、Stack等。

三、多线程

1、线程池中有几种状态,分别是如何变化的?

(1)RUNNING:会接收新任务并且会处理队列中的任务。

(2)SHUTDOWN:不会接受新任务但是会处理队列中的任务,任务处理完后会中断所有线程。

(3)STOP:不会接收新任务并且不会处理队列中的任务,会直接中断所有线程。

(4)TIDYING:所有线程都停止了之后,线程池的状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated()。

(5)TERMINATED:terminated()执行完之后就会转变为TERMINATED。

这五种状态并不能任意转换,只会有以下几种转换情况:

(1)RUNNING—>SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()。

(2)RUNNING—>STOP:手动调用shutdownNow()触发。

(3)SHUTDOWN—>STOP:手动先调用shutdown()紧着调用shutdownNow()触发。

(4)SHUTDOWN—>TIDYING:线程池所有线程都停止后自动触发。

(5)STOP—>TIDYING:线程池所有线程都停止后自动触发。

(6)TIDYING—>TERMINATED:线程池自动调用terminated()后触发。

2、线程的生命周期,线程有哪些状态?

线程通常有五种状态:创建、就绪、运行、阻塞和死亡。

(1)新建状态(New):新创建了一个线程对象。

(2)就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

(3)运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

(4)阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

(5)死亡状态(Dead):线程执行完了或者因异常退出了run方法, 该线程结束生命周期。

阻塞的情况又分为三种:

(1)等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是Object类的方法。

(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。

(3)其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法。

3、线程的状态流转?

(1)NEW:新建但是尚未启动的线程处于此状态,没有调用 start() 方法。

(2)RUNNABLE:包含就绪(READY)和运行中(RUNNING)两种状态。线程调用start()方法会进入就绪(READY)状态,等待获取CPU时间片。如果成功获取到CPU时间片,则会进入运行中(RUNNING)状态。

(3)BLOCKED:线程在进入同步方法/同步块(synchronized)时被阻塞,等待同步锁的线程处于此状态。

(4)WAITING:无限期等待另一个线程执行特定操作的线程处于此状态,需要被显示的唤醒,否则会一直等待下去。例如对于Object.wait(),需要等待另一个线程执行Object.notify()或Object.notifyAll();对于Thread.join(),则需要等待指定的线程终止。

(5)TIMED_WAITING:在指定的时间内等待另一个线程执行某项操作的线程处于此状态。跟WAITING类似,区别在于该状态有超时时间参数,在超时时间到了后会自动唤醒,避免了无期限的等待。

(6)TERMINATED:执行完毕已经退出的线程处于此状态。

线程在给定的时间点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。

4、如何优雅的停止一个线程?

可以使用stop()方法,但是stop()方法太粗暴了,一旦调用了stop(),就会直接停掉线程,这样就可能造成严重的问题,比如任务执行到哪一步了?该释放的锁释放了没有?都存在疑问。

stop()会释放线程占用的synchronized锁,而不会自动释放ReentrantLock锁。

可以使用中断来停止线程。

比如,可以通过控制变量在大于某一个数时停止,不然就算中断了也不会停止。此外,线程池中也是通过interrupt()来停止线程的。

5、线程池的核心线程数、最大线程数该如何设置?

对于CPU密集型任务,线程数最好就等于CPU核心数,但是为了应对线程执行过程发生缺页中断或其他异常导致线程阻塞的请求,可以额外在多设置一个线程,这样当某个线程暂时不需要CPU时,可以有替补线程来继续利用CPU。所以,对于CPU密集型任务,可以设置线程数为:CPU核心数+1

对于IO密集型任务,线程在执行IO型任务时,可能大部分时间都阻塞在IO中。假设现在有10个CPU,如果我们只设置了10个线程来执行IO型任务,那么很有可能这10个线程都阻塞在了IO上,这样这10个CPU就都没活干了,所以,对于IO型任务,通常会设置线程数为: 2*CPU核心数

但是这不一定是最佳的,比如,有10个CPU,线程数为20,那么就有可能这20个线程同时阻塞在了IO上,所以可以再增加线程,从而去压榨CPU的利用率。

通常,如果IO型任务执行的时间越长,那么同时阻塞在IO上的线程就可能越多,这样就可以设置更多的线程,但是,线程肯定不是越多越好,可以采用公式计算,公式为:线程数=CPU核心数*(1+线程等待时间/线程运行总时间)。

线程等待时间:指的就是线程没有使用CPU的时间,比如阻塞在IO。

线程运行总时间:指的是线程执行完某个任务的总时间。

可以利用jvisualvm抽样来估计这两个时间。

6、用过什么线程池,核心线程数量和最大线程数量一致会发生什么情况?

最常用的就是ThreadPoolExecutor类。

当核心线程数量和最大线程数量设置为一致时,线程池不会创建新的线程,除非现有的线程因超时或异常退出而被移除。这种情况下,线程池将一直维持固定数量的线程,不会根据任务负载的变化动态调整线程数。

(1)资源浪费: 如果任务提交速度很快,线程池中的线程可能会一直保持在最大线程数,导致资源浪费,降低系统性能。

(2)无法应对突发流量: 如果在短时间内出现大量任务提交,而线程池的大小一直保持不变,可能无法及时处理所有任务,导致响应时间增加。

7、线程池的工作流程?

1、创建线程池:可以通过调用相应的线程池构造函数来实现。在创建线程池时,需要指定线程池的大小,即可以容纳的线程数量。

2、提交任务:一旦线程池创建成功,就可以向线程池提交任务。任务可以是实现了Runnable接口或Callable接口的对象。线程池会根据任务的类型来执行相应的操作。

3、任务调度:线程池会根据任务的提交顺序和线程池的状态来调度任务的执行。当有任务提交时,线程池会选择一个空闲的线程来执行任务。如果所有线程都在执行任务,而且线程池的大小已经达到上限,新提交的任务将会进入等待队列,等待有空闲线程时再执行。

4、线程执行任务:线程池中的线程会从等待队列中获取任务并执行。线程执行任务的过程包括调用任务的run方法或call方法,并处理任务的返回结果。

5、任务完成:当任务执行完成后,线程会返回线程池,并准备接受新的任务。线程池会根据需要继续调度任务的执行,直到线程池被显式关闭。

8、为什么要用线程池?优缺点?解释下线程池参数?

如果我们在方法中直接new一个线程来处理,当这个方法被调用频繁时就会创建很多线程,会消耗系统资源,降低系统的稳定性。

(1)降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,使用线程池可以统一分配,调优和监控。

线程池的缺点:

(1)可能引发死锁问题。

(2)任务分配可能不均匀:某些线程可能会处理更多的任务,而其他线程可能会空闲。

(3)资源消耗:线程池本身也需要占用一定的内存和CPU资源。

线程池参数介绍:

(1)核心线程数:当线程池运行的线程少于corePoolSize时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。

(2)最大线程数:线程池允许开启的最大线程数。

(3)保持存活时间:如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过keepAliveTime时会被终止。

(4)任务队列:用于保留任务并移交给工作线程的阻塞队列。

(5)线程工厂:用来生产线程执行任务。可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然也可以选择自定义线程工厂,一般会根据业务来制定不同的线程工厂。

(6)任务拒绝策略:往线程池添加任务时,会在两种情况下触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时。

9、线程池有哪些拒绝策略?

(1)AbortPolicy:中止策略。默认的拒绝策略,直接抛出RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。

(2)DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。

(3)DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。

(4)CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者,由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。

10、线程池有哪些队列?

(1)ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出对元素进行排序。

(2)LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool使用了该队列。

(3)SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入 SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。Executors.newCachedThreadPool使用了该队列。

(4)PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序。元素的优先级是通过自然顺序或 Comparator 来定义的。

11、使用队列有什么需要注意的吗?

使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。

使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。

12、线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程?

一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源。

在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

13、线程只能在任务到达时才启动吗?

默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。

14、核心线程怎么实现一直存活?

核心线程在获取任务时,通过阻塞队列的take()方法实现一直阻塞(存活)。

15、非核心线程如何实现在keepAliveTime后死亡?

利用阻塞队列的方法,在获取任务时通过阻塞队列的poll(time,unit) 方法实现延迟死亡。

16、非核心线程能成为核心线程吗?

线程池内部是不区分核心线程和非核心线程的。只是根据当前线程池的工作线程数来进行调整。

17、如何终止线程池?

(1)shutdown:“温柔”的关闭线程池。不接受新任务,但是在关闭前会将之前提交的任务处理完毕。

(2)shutdownNow:“粗暴”的关闭线程池,也就是直接关闭线程池,通过Thread#interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。

18、有哪些线程池?

(1)newFixedThreadPool:固定线程数的线程池。corePoolSize = maximumPoolSize,keepAliveTime为0,工作队列使用无界的LinkedBlockingQueue。适用于为了满足资源管理的需求,而需要限制当前线程数量的场景,适用于负载比较重的服务器。

(2)newSingleThreadExecutor:只有一个线程的线程池。corePoolSize = maximumPoolSize = 1,keepAliveTime为0, 工作队列使用无界的LinkedBlockingQueue。适用于需要保证顺序的执行各个任务的场景。

(3)newCachedThreadPool:按需创建新线程的线程池。核心线程数为0,最大线程数为 Integer.MAX_VALUE,keepAliveTime为60秒,工作队列使用同步移交 SynchronousQueue。该线程池可以无限扩展,当需求增加时,可以添加新的线程,而当需求降低时会自动回收空闲线程。适用于执行很多的短期异步任务,或者是负载较轻的服务器。

(4)newScheduledThreadPool:创建一个以延迟或定时的方式来执行任务的线程池,工作队列为 DelayedWorkQueue。适用于需要多个后台线程执行周期任务。

(5)newWorkStealingPool:用于创建一个可以窃取的线程池,底层使用 ForkJoinPool 实现。

19、线程池里有个ctl,是如何设计的?

ctl是一个打包两个概念字段的原子整数。

(1)workerCount:指示线程的有效数量;

(2)runState:指示线程池的运行状态,有RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED等状态。

int类型有32位,其中ctl的低29为用于表示workerCount,高3位用于表示runState。

20、ctl为什么这么设计?有什么好处吗?

ctl这么设计的好处是将runState和workerCount的操作封装成一个原子操作。

runState和workerCount是线程池正常运转中的2个最重要属性,线程池在某一时刻该做什么操作,取决于这2个属性的值。

因此无论是查询还是修改,都必须保证对这2个属性的操作是属于“同一时刻”的,也就是原子操作,否则就会出现错乱的情况。如果使用2个变量来分别存储,要保证原子性则需要额外进行加锁操作,这显然会带来额外的开销,而将这2个变量封装成1个 AtomicInteger 则不会带来额外的加锁开销,而且只需使用简单的位操作就能分别得到 runState 和 workerCount。

21、线程池中线程的复用原理?

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。

22、说说你对守护线程的理解?

守护线程:为所有非守护线程提供服务的线程,任何一个守护线程都是整个JVM中所有非守护线程的保姆。

守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,不会管守护线程,直接把它中断。

注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为他不靠谱。

守护线程的作用是什么?

比如,GC垃圾回收线程:当程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:(1)来为其它线程提供服务支持的情况。(2)或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比如,数据库录入或者更新,这些操作都是不能中断的。

用法:thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。不能把正在运行的常规线程设置为守护线程。

在守护线程中产生的新线程也是守护的。

守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。

Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用守护线程就不能用Java的线程池。

23、说说你对线程安全的理解?

指的不是线程安全,应该是内存安全,因为在JVM中堆是共享内存,可以被所有线程访问。

1
当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,就说这个对象是线程安全的。

堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

1
在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈相互独立。因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。

目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程,这是由操作系统保障的。

在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。

24、线程池中提交一个任务的流程是怎么样的?

(1)使用execute()方法提交一个Runnable对象;

(2)会先判断当前线程池中的线程数是否小于corePoolSize;

(3)如果小于,则创建新线程并执行Runnable;

(4)如果大于等于,则尝试将Runnable加入到workQueue中;

(5)如果workQueue没满,则将Runnable正常入队,等待执行;

(6)如果workQueue满了,则会入队失败,那么会去尝试继续增加线程;

(7)如果当前线程池中的线程数是否小于maximumPoolSize;

(8)如果小于,则创建新线程并执行任务;

(9)如果大于等于,则执行拒绝策略,拒绝此Runnable。

25、wait()和sleep()的区别?

sleep是Thread类的静态本地方法,wait是Object类的本地方法。

(1)对于同步锁的影响不同:如果当前线程持有同步锁,那么sleep是不会让线程释放同步锁的。wait()会释放同步锁,让其他线程进入synchronized代码块执行。

(2)使用范围不同:sleep()可以在任何地方使用。wait()只能在同步控制方法或者同步控制块里面使用,否则会抛IllegalMonitorStateException。

(3)恢复方式不同:两者会暂停当前线程,但是在恢复上不太一样。sleep() 在时间到了之后会重新恢复;wait() 则需要其他线程调用同一对象的 notify()/ nofityAll()才能重新恢复。

26、线程的sleep()方法和yield()方法有什么区别?

线程执行sleep()方法后进入超时等待状态,而执行 yield() 方法后进入就绪状态。

sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会。

yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。

27、线程的join()方法是干啥的?

join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程。

28、Thread调用start()方法和调用run()方法的区别?

run():普通的方法调用,在主线程中执行,不会新建一个线程来执行。

start():新启动一个线程,这时此线程处于就绪状态,并没有运行,一旦得到CPU时间片,就开始执行run()方法。

29、如何理解Java并发中的可见性?

Java并发可见性指的是多线程并发访问共享变量时,对变量的更改能够被其他线程及时感知, 即在一个线程修改变量后,其他线程能够立即看到这个变量的修改结果。

在Java中,可以使用volatile关键字来保证变量的可见性,对于加了volatile的变量,线程在读取该变量时会直接从内存中读取,再修改该变量时会同时修改CPU高速缓存和内存中的值。

30、如何理解Java并发中的原子性?

Java并发原子性指的是在多线程并发的情况下,一段代码或操作要么完全执行成功,要么完全不执行,不出现执行一半被其他线程打断或干扰的情况。换句话说,就是对同一变量的多个操作能够像原子操作一样,保证多线程环境下的数据一致性,避免出现数据竞争和脏数据等问题。

由于CPU、内存、IO(磁盘、网络)之间的性能差距,为了能充分利用CPU,当线程执行IO操作时,线程会让出CPU,使得CPU去执行其他线程的指令,并且本身来说,为了达到线程并发执行的效果,CPU也会固定时间片来切换执行不同线程。

Java中需要通过各种锁机制来保证原子性。

31、如何理解Java并发中的有序性?

Java并发有序性指的是多个线程执行的指令和操作,按照开发者编写程序的顺序或者预定的顺序进行执行。多线程并发执行时,可能会发生指令的重排,导致程序的执行顺序与预期不一致,从而出现数据竞争和线程安全问题。

编译器有时为了进行编译优化,会进行指令重排序,比如:

1
new Person();

这行代码分三步:

(1)申请内存空间;

(2)在内存空间初始化Person对象相关的内容;

(3)返回内存空间地址。

但是编译有可能会优化为:

(1)申请内存空间;

(2)返回内存空间地址。

(3)在内存空间初始化Person对象相关的内容;

可以通过锁机制或者volatile来保证有序性。

32、多线程中有哪些锁?怎么用?

(1)synchronized锁,通过synchronized关键字可以对代码块或方法进行加锁,保证同一时刻只有一个线程可以访问被锁定的代码段。

(2)可重入锁,ReentrantLock是java.util.concurrent.locks包中提供的锁实现。它允许线程在同一时刻多次获得同一个锁,是一个可重入锁。

(3)读写锁ReentrantReadWriteLock,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。它包括一个读锁和一个写锁。

(4)StampedLock是Java 8引入的新锁机制,提供了乐观锁和悲观锁两种模式。它比ReentrantReadWriteLock 更加灵活,但也更加复杂。

33、乐观锁?

(1)基本思想:乐观锁假设在绝大多数情况下,共享资源不会发生冲突,因此允许多个线程同时访问和修改共享资源,但在更新时会先检查版本信息,以确保数据的一致性。

(2)实现方式:通常通过给共享资源添加一个版本号(或者时间戳)来实现,每次更新时,都会比较版本号是否一致,如果一致则执行更新操作,否则拒绝更新。

(3)应用场景:适用于读取频繁、写入少量的场景,以避免加锁导致的性能损耗。适用于冲突较少的情况,例如读取某个数据的同时,可能只有极小的概率会有其他线程修改该数据。

34、悲观锁?

(1)基本思想:悲观锁假设在大多数情况下,共享资源会发生冲突,因此在访问共享资源之前会先加锁,确保其他线程无法同时访问。

(2)实现方式:通常使用各种类型的锁来实现,如内置锁(synchronized)、ReentrantLock等。

(3)应用场景:适用于写入操作较频繁的情况,因为悲观锁能确保在对共享资源进行写操作时,其他线程无法同时访问。适用于冲突较多的情况,例如涉及到复杂的业务逻辑或需要长时间保持共享资源的独占。

35、如何选择?

(1)选择乐观锁时,需要确保在冲突发生时能够进行合适的处理,例如重新尝试、回滚等。

(2)选择悲观锁时,要注意避免锁的粒度过大,以减少锁竞争的可能性。

四、高并发

1、并发的三大特性?

(1)原子性:指在一个操作中CPU不可以在中途暂停,然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。AtomicInteger(保证i++) CAS 【synchronized】

(2)可见性(synchronized):当多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。(底层协议:总线Lock,和MESI保证可见性)【volatile、final、synchronized】

(3)有序性:虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。【volatile、synchronized】

2、ThreadLocal的底层原理和使用场景?

ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象中都存在一个ThreadLocalMap,key为ThreadLocal对象,value为需要缓存的值。

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储到ThreadLocalMap对象中。

get方法执行过程类似。ThreadLocal首先会获取当、、前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

使用场景:

(1)在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

(2)线程间数据隔离。

(3)进行事务操作,用于存储线程事务信息。

(4)数据库连接,Session会话管理。

Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离。

3、ThreadLocal内存泄漏原因,如何避免?

ThreadLocal内存泄漏的根源是:因为当ThreadLocal对象使用完之后,应该把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏的问题。

ThreadLocal正确的使用方法:

(1)每次使用完ThreadLocal都调用它的remove()方法清除数据。

(2)将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

如果key为强引用,会出现什么问题?

当ThreadLocalMap的key为强引用,回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致内存泄露。

key使用弱引用,会怎么样呢?

当ThreadLocalMap的key为弱引用,回收ThreadLocal时,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

4、Java的四种引用类型?

强引用:一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

软引用:用于描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会回收软引用指向的对象。软引用通常用于实现内存敏感的缓存。

虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾收集器回收。

5、Synchronized的底层原理?

synchronized的底层实现主要区分:方法和代码块。

synchronized修饰代码块时,编译后会生成 monitorenter和monitorexit指令,分别对应进入同步块和退出同步块。会有两个monitorexit,这是因为编译时JVM为代码块添加隐式的try-finally,在finally中进行了锁释放,这也是为什么synchronized不需要手动释放锁的原因。

synchronized修饰方法时,编译后会生成ACC_SYNCHRONIZED标记,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED访问标志是否被设置,如果设置了则会先尝试获得锁。

两种实现其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

6、Synchronized底层维护了几个链表存放被阻塞的线程?

synchronized底层对应的JVM模型为objectMonitor,使用了3个双向链表来存放被阻塞的线程:_cxq(Contention queue)、_EntryList、WaitSet。

当线程获取锁失败进入阻塞后,首先会被加入到_cxq链表,_cxq链表的节点会在某个时刻被进一步转移到EntryList链表。

当持有锁的线程释放锁后,EntryList链表头结点的线程会被唤醒,该线程称为successor(假定继承者),然后该线程会尝试抢占锁。

当调用wait()时,线程会被放入WaitSet,直到调用了notify()/notifyAll()后,线程才被重新放入cxq或EntryList,默认放入cxq链表头部。

7、为什么释放锁时被唤醒的线程被称为”假定继承者“?被唤醒的线程一定能获取到锁吗?

因为被唤醒的线程并不是就一定获取到锁了,该线程仍然需要去竞争锁,而且可能会失败,所以该线程并不是就一定会成为锁的”继承者“,而只是有机会成为,所以称它为假定的。

这是 synchronized 为什么是非公平锁的一个原因。

8、Synchronized 为什么是非公平锁?非公平体现在哪些地方?

(1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:

​ 1)先将锁的持有者owner属性赋值为null;

​ 2)唤醒等待链表中的一个线程。

在1和2之间,如果有其他线程刚好在尝试获取锁,则可以马上获取到锁。

(2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表就会先被唤醒。

9、notifyAll 是怎么实现全唤起的?

notify是获取WaitSet的头结点,执行唤起操作。

notifyAll的流程就是循环遍历WaitSet的所有节点,对每个节点执行notify操作。

10、Synchronized各种加锁场景的作用范围?

(1)作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。

1
public synchronized void method(){}

(2)作用于静态方法,锁住的是类的Class对象,因为Class的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。

1
public static synchronized void method(){}

(3)作用于Lock.class,锁住的是Lock的Class对象,也是全局只有一个。

1
synchronized (Lock.class) {}

(4)作用于this,锁住的是对象实例,每一个对象实例有一个锁。

1
synchronized (this) {}

(5)作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。

1
2
3
public static Object monitor = new Object();

synchronized (monitor) {}

11、Synchronized和Lock的区别?

(1)Lock是一个接口;synchronized是Java中的关键字。

(2)Lock在发生异常时,如果没有主动通过unLock()去释放锁,很可能会造成死锁现象,因此使用Lock时需要在finally块中释放锁;synchronized不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生。

(3)Lock的使用更加灵活,可以有响应中断、有超时时间等;而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,直到获取到锁。

13、为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?

而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。

重量级锁底层依赖于系统的同步函数来实现,在linux中使用pthread_mutex_t(互斥锁)来实现。

这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。

14、偏向锁的好处?

偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次CAS操作修改markword ,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的CAS操作。

如果确定同步代码块会被多个线程访问或者竞争较大,可以通过-XX:-UseBiasedLocking参数关闭偏向锁。

15、偏向锁、轻量级锁、重量级锁分别对应了什么使用场景?

(1)偏向锁:适用于只有一个线程获取锁。当第二个线程尝试获取锁时,即使此时第一个线程已经释放了锁,此时还是会升级为轻量级锁。但是有一种特例,如果出现偏向锁的重偏向,则此时第二个线程可以尝试获取偏向锁。

(2)轻量级锁:适用于多个线程交替获取锁。跟偏向锁的区别在于可以有多个线程来获取锁,但是必须没有竞争,如果有则会升级会重量级锁。

(3)重量级锁:适用于多个线程同时获取锁。

16、自旋发生在哪个阶段?

轻量级锁阶段并没有自旋操作,在轻量级锁阶段,只要发生竞争,就是直接膨胀成重量级锁。而在重量级锁阶段,如果获取锁失败,则会尝试自旋去获取锁。

17、为什么要设计自旋操作?

因为重量级锁的挂起开销太大。一般来说,同步代码块内的代码应该很快就执行结束,这时候竞争锁的线程自旋一段时间是很容易拿到锁的,这样就可以节省了重量级锁挂起的开销。

18、自适应自旋是如何体现自适应的?

自适应自旋锁有自旋次数限制,范围在:1000~5000。如果当次自旋获取锁成功,则会奖励自旋次数100次,如果当次自旋获取锁失败,则会惩罚扣掉次数200次。所以如果自旋一直成功,则JVM认为自旋的成功率很高,值得多自旋几次,因此增加了自旋的尝试次数。相反的,如果自旋一直失败,则JVM认为自旋只是在浪费时间,则尽量减少自旋。

19、Synchronized 锁能降级吗?

可以。具体的触发时机是在全局安全点中,执行清理任务的时候会触发尝试降级锁。当锁降级时,主要进行了以下操作:

(1)恢复锁对象的markword对象头;

(2)重置ObjectMonitor,然后将该ObjectMonitor放入全局空闲列表,等待后续使用。

20、CAS是什么?

CAS需要3个操作数:内存地址V,旧的预期值A,和即将要更新的目标值B。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

CAS的使用流程为:1)首先从地址V读取值A;2)根据A计算目标值B;3)通过CAS以原子的方式将地址V中的值从A修改为B。

CAS的缺点,存在三大问题:

(1)循环时间长开销很大;(CAS通常是配合无线循环一起使用的,比如,getAndAddInt方法执行时,如果CAS失败,会一直进行尝试;如果CAS长时间一直不成功,可能会给CPU带来很大的开销。)

(2)只能保证一个变量的原子操作;(当对一个变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个变量操作时,CAS目前无法直接保证操作的原子性,但是可以通过以下两种方法解决:1、使用互斥锁来保证原子性,2、将多个变量封装成对象,通过AtomicReference来保证原子性。)

(3)ABA问题。在第1步中读取的值是A,并且在第3步修改成功了,并不能确定它的值在第1步和第3步之间有没有被其他线程改变。如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清除“ABA“问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

21、Synchronized和ReentrantLock的区别?

(1)sychronized是一个关键字,ReentrantLock是一个类。

(2)sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁。

(3)sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁。

(4)sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁。

(5)sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态。

(6)sychronized底层有一个锁升级的过程。

22、ReentrantLock中的公平锁和非公平锁的底层实现?

首先不管是公平锁还是非公平锁,它们的底层实现都会使用AQS来进行排队,区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队;如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

ReentrantLock默认是非公平锁,性能更高一点。

可重入锁:同一个线程可以多次获取同一个锁,而不会造成死锁。

23、ReentrantLock中tryLock()和lock()方法的区别?

(1)tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false。

(2)lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值。

24、CountDownLatch和Semaphore的区别和底层原理?

CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果获取不到,线程会阻塞,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。

25、CountDownLatch和CyclicBarrier的区别?

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

  1. CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
  2. CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。
  3. CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

26、ReentrantLock锁的是对象吗?在分布式中可以用吗?

ReentrantLock是基于对象的,但并不是直接锁住对象本身,而是使用一个实例来管理锁的状态。具体地,它是通过代码中int类型的state标识来标识锁的状态。

在分布式环境中,ReentrantLock通常只用于单个Java虚拟机内的多线程同步,它不适用于分布式系统。在分布式环境中,因为不同的节点之间无法直接共享Java对象,所以ReentrantLock无法实现跨节点的锁定。

在分布式环境中,常见的分布式锁有:

(1)基于数据库的分布式锁:可以利用数据库的事务特性来实现分布式锁。通过在数据库中创建一个专门的锁表,不同的节点可以通过竞争数据库行级锁或者悲观锁来获取锁。

(2)基于Redis的分布式锁:通过在缓存中存储锁的状态信息,节点可以通过竞争缓存中的某个键来获取锁。常见的实现方式包括使用Redis的SETNX(set not exists)命令来创建锁。(key是锁名称,value是谁抢到了锁)

(3)基于Zookeeper的分布式锁:Zookeeper是一个分布式协调服务,可以用于实现分布式锁。节点可以通过在Zookeeper上创建临时有序节点来竞争锁。获取锁的节点是创建最小序号节点的节点,这种方式确保了锁的有序性。

(4)基于消息队列的分布式锁:通过在消息队列中发送锁请求和锁释放消息,可以实现分布式锁。节点可以订阅消息队列来获取锁的状态变化。

27、如何检测死锁?

死锁的四个必要条件:

(1)互斥条件:进程对所分配到的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

(2)请求和保持条件:进程已经获得了至少一个资源,但又对其他资源发出请求,而该资源已被其他进程占有,此时该进程的请求被阻塞,但又对自己获得的资源保持不放。

(3)不可剥夺条件:进程已获得的资源在未使用完毕之前,不可被其他进程强行剥夺,只能由自己释放。

(4)环路等待条件:存在一种机进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{P1,P2,…,Pn},其中Pi等待的资源被P(i+1)占有(i=0,1,…,n-1),Pn等待的资源被P0占有。

一个线程需要同时获得多把锁。

死锁诊断(idea自带):jps(查看运行的线程)、jstack(查看线程运行的情况)、可视化工具:jconsole、Visual VM。

28、怎么预防死锁?

预防死锁的方式就是打破四个必要条件中的任意一个即可。

(1)打破互斥条件:在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件。

(2)打破请求和保持条件:1)采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待。 2)每个进程提出新的资源申请前,必须先释放它先前所占有的资源。

(3)打破不可剥夺条件:当进程占有某些资源后又进一步申请其他资源而无法满足,则该进程必须释放它原来占有的资源。

(4)打破环路等待条件:实现资源有序分配策略,将系统的所有资源统一编号,所有进程只能采用按序号递增的形式申请资源。

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.