Skip to content

面试八股

计算机网络

HTTPS和HTTP的区别是什么?

HTTP和HTTPS是两种协议,分别是Hypertext Transfer Protocol和HyperText Transfer Protocal Secure。 HTTPS还经常被称之为HTTP over SSL或者HTTP over TSL,HTTPS经由HTTP进行通信,但利用SSL/TLS来加 密数据包。

  1. 安全性: HTTP:HTTP是明文传输的,这意味着数据在传输过程中不加密,容易受到中间人攻击。敏感信息,如密 码和信用卡号,如果通过HTTP传输,可能会被窈取。

  2. HTTPS: HTTPS使用SSL (Secure Sockets Layer)或其继任者TLS (Transport Layer Security)来加 密数据传输,使数据在传输过程中加密,更难被中间人攻击窈取。

  3. URL: • HTTP: HTTP的URL以http://开头 • HTTPS:HTTPS的URL以https://开头

  4. 证书:

    HTTP: HTTP不需要使用效字证书。

HTTPS: HTTPS需要使用效字证书,这个证书由受信任的第三方机构(如CA,Certificate Authority)颁 发,用于验证网站的身份。

  1. 默认端口: 。HTTP: 默认端口为80。 。 HTTPS:默认端口为443。
  2. 性能: HTTP:由于不需要加密和解密数据,HTTP的性能通常比HTTPS更高。这在某些情况下可以使HTTP成为 更好的选择,尤其是对于不涉及敏感信息的静态内容传输。

HTTPS:HTTPS需要进行加密和解密操作,这会增加一些计算开销,但现代计算机和服务器通常能够很 好地处理这种负担。

SSL各个版本都存在安全漏洞,目前TLS 1.2和TLS 1.3是目前最广泛使前用的比较少用的版本,因为它们提供更高的安全性。

TLS的性能通常比SSL更好,尤其是TLS1.2和TLS1.3版本,因为它们引 入了更有效的加密算法和协议优化。

HTTPS建立连接的时候是几次握手?

需要TCP的3次握手,在根据TLS的版本,做2-4步的加密通道建立(TLS 1.2需要4步,TLS 1.3需要2步)。 首先是需要进行TCP的三次握手,用来建议TCP的连接 •第一次握手:客户端发送 SYN 包(同步序列编号)到服务器,进入 SYN_SENT 状态。 •第二次握手:服务器返回 SYN=ACK 包(确认客户端的SYN),进入 SYN_RCVD 状态。 •第三次握手:客户端发送 ACK包确认服务器的SYN,完成TCP连接建立。

接下来通过TLS来建立加密通道,根据不同的版本看,情況不一样。以TLS1.2为例(需2个往返,共4步):

  • ClientHello:发送客户端支持的TLS版本、加密算法、随机数。
  • ServerHello:服务器选定TLS版本、加密算法、随机数:发送证书(身份验证)、ServerKeyExchang e(密钥參数,如ECDHE)。
  • 客户端验证证书:生成预主密钥,用服务器公钥加密后发送;计算会话密钥。
  • Finish:双方发送加密的 Finished 消息验证掘手完整性。

对于TLS 1.3来说,做了优化(1个往返,共2步):

ClientHello:包含支持的加密算法和密钥共享(Key Share)。

ServerHello:选择参数、发送证书、生成会话密钥并直接响应。

什么是TCP的粘包、拆包问题?

TCP粘包和拆包问题是指在进行TCP通信时,因为TCP是面向流的,所以发送方在传输数据时可能会将多个小的数 据包粘合在一起发送,而授收方则可能将这些数据包拆分成多个小的数据包进行授收,从而导致数据接收出现错误 或者数据粘连的问题。

TCP粘包和拆包问题主要出现在以下两种情况下: 1.发送方连续发送多个小数据包:由于TCP是基于流的协议,发送方在传输数据时可能会将多个小數据包组合成 一个大数据包进行发送,从而导致接收方在接收数据时无法区分不同数据包之间的界限。 2.接收方缓存区大小限制:接收方在接收数据时,如果接收缓存区的大小有限,可能会将一个大的数据包拆分成 多个小数据包进行授收,从而导致粘包和拆包问题的出现。

什么是TCP三次握手、四次挥手?

  1. 你(客户端)给一个朋友(服务器)打电话,告诉他你想开始对话。这就像是发送一个SYN(同步序列编号)信号,表示你想开始建立连接。(client向server发送syn,seq=X,此时client验证dlient发送能力正常。client置为SYN SENT状态)
  2. 你的朋友接到电话,明白你想开始对话。他回应说“好的,我准备好了”,同时也告诉你他也想说些话。这就 相当于服务器发送SYN-ACK(同步和确认)信号,既确认收到了你的请求,也表明它准备好了并想建立连 接。(server收到syn,此时server验证client发送能力正常,server接收能力正常。server向client发送ack= x +1,seq =y,此时server验证server发送能力正常。server置为SYN_RCVD状态)
  3. 最后,你回复你的朋友说你收到了他的确认,现在可以开始对话了。这就是发送ACK(确认)信号,确认你 已经准备好进行通信。(client收到ack,此时client验证client接收能力正常,server接收发送能力正常。client 向server发送ack=y +1, seq =X + 1, server接收到后验证client接收能力正常。client置为ESTABLISHED 状态)

1.你(客户端)和朋友(服务器)通话结束后,告诉他你想挂电话了。这就像是发送一个FIN(结束)信号,表 示你想结束这次连接。(client向server发送fin。 client置为FIN_WAIT_1) 2.朋友听到你想挂电话了,他回应说“知道了,但我还有点事情要处理”,即使他知道对话即将结束。这就相当 于服务器发送ACK(确认)信号,确认收到了你想结束连接的请求,但可能还需要一些时间来处理剩余的数 据。(server向client发送ack。 server置为CLOSE_WAIT, client置为FIN_WAIT_2) 3.一段时间后,你的朋友处理完了他的事情,这时他打电话告诉你他也准备好挂电话了。这是服务器端发送第二 个FIN信号,表明他现在也准备好结束这次连接。(等server传输数据完毕后,向client发送fin。server置为 LAST_ACK) 4.最后,你回复说你收到了他的消息,并同意现在可以挂电话了。这就是发送最后一个ACK信号,确认收到服 务器端的结束请求。(client向server发送ack。client置为TIME_WAIT。之后等待2MSL,dlient关闭。server 接收到后置为CLOSED)

get和post的区别?至少四点

基于HTTP协议规范(RFC 7231)的定义定义上看,

  • GET:用于获取/请求数据。它本质上是“只读”操作,不应该用于产生“副作用”(如修改数据)。它的目的是从服务器获取资源,像是查询数据库。
  • POST:用于提交/创建数据。它通常会在服务器上产生副作用,比如更新数据或创建新资源。它的目的是向服务器提交数据,像是提交表单。

参数携带方式

  • GET:参数通过URL传递。所有参数都以 ?key1=value1&key2=value2 的形式拼接在URL后面,因此可见、可被缓存、可被收藏为书签
  • POST:参数通过请求体传递。数据被放在HTTP请求的Body部分,对用户不可见,也不会显示在URL中。

数据长度限制

  • GET:由于参数在URL中,而URL长度是有限制的(这个限制主要来自浏览器和服务器,而非HTTP协议本身)。不同浏览器对URL长度的支持从几千字节到几兆字节不等,但通常不建议传递过长参数。
  • POST:理论上没有长度限制,因为数据在请求体中。可以传输大量数据,比如上传文件。

安全和幂等

  • GET是幂等的。意思是多次执行相同的GET请求,效果和结果都是一样的,不会对服务器资源状态产生改变。因此,GET请求可以被浏览器缓存、重试。
  • POST是不幂等的。多次提交同一个POST请求(比如下单),可能会在服务器上创建多个资源,产生额外的效果。因此,浏览器在刷新时会提示你是否重新提交表单。

java

Hashtable 是一个过时的、线程安全的类,而 HashMap 是现代Java中默认的、高性能的非线程安全实现。在需要线程安全时,我们通常会用 ConcurrentHashMap 来替代 Hashtable

treemap简介

TreeMap基于红黑树实现,保证了所有键值对的有序性(按键的自然顺序或自定义比较器顺序)。

  • 所有操作(增删改查)的时间复杂度都是O(log n)
  • 自动维护键的排序
  • 支持高效的范围查询和导航操作

典型应用场景:

  1. 排行榜系统 - 按分数自动排序
  2. 价格区间查询 - 快速找到指定价格范围的商品
  3. 事件调度 - 按时间顺序执行任务
  4. 字典应用 - 按字母顺序存储和检索单词
  5. 统计分析 - 进行数据分段统计

两个+号拼接了三个string创建了几个对象?

String result = "a" + "b" + "c";-0个新对象,编译时会直接优化掉

如果是变量拼接,会创建 2 个对象(一个 StringBuilder 和一个最终的 String 对象)。 如果是字面量直接拼接如 "a"+"b"+"c",则编译期合并,只涉及常量池 1 个字符串(且可能早已存在),不创建新对象。

static和final

static类级别而非实例级别

  • 静态变量:全局唯一,所有对象共享。
  • 静态方法:只能访问静态成员,不能使用this
  • 静态块:类加载时执行一次。

final

  • 不可改变
  • final变量:值/引用不可变。
  • final方法:不可重写。
  • final类:不可继承。

黄金组合static final 创建全局常量。

接口和抽象类的区别?

特性接口抽象类
定义关键字interfaceabstract class
方法实现Java 8前只能抽象方法,之后可有默认方法可以有抽象方法和具体方法
成员变量默认 public static final(常量)任意类型的成员变量
构造方法不能有构造方法可以有构造方法
继承方式implements(可实现多个)extends(只能继承一个)
设计理念"有什么能力""是什么"

arraylist和linkedlist的差异和各自的使用场景?

  • ArrayList:基于动态数组。它内部维护着一个数组,当数组容量不足时,会创建一个新的更大数组,并将旧数组的数据拷贝过去。
  • LinkedList:基于双向链表。每个元素(节点)都包含数据本身以及指向前一个节点和后一个节点的引用。

比如要快速访问遍历填充数据,从数据库查询出来数据,基本上用的linkedList。

linkedList一般很少用,一般用作队列、业务环境下,一般只有特殊的需求,比如发现一笔流水数据,需要将数据红冲掉,会补一笔相反金额的特殊流水,插入更正记录。

linkedlist是单向链表还是双向链表?为什么?

双向链表

  1. 功能完整性:支持双向遍历和双端操作
  2. 性能优化:在已知节点位置时,插入删除都是 O(1)
  3. 接口实现:完美实现 List 和 Deque 接口的所有方法

List,Set以及Map等集合体系详解

  • List,Set,Map都是接口,前两个继承至CollectionQ接口,Map为独立接口
  • Set下有HashSet,LinkedHashSet,TreeSet
  • List下有ArrayList,Vector,LinkedList
  • Map下有Hashtable,LinkedHashMap,HashMap,TreeMap
  • Collection接口下还有个Queue接口,有PriorityQueue

什么叫做泛型?

Java泛型 (generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架 中。 泛型的好处有两个: 1.方便:可以提高代码的复用性。以List接口为例,我们可以将String、Integer等类型放入List中,如不用泛 型,存放String类型要写一个List接口,存放Integer要写另外一个List接口,泛型可以很好的解决这个问题

2.安全:在泛型出之前,通过Object实现的类型转换帮要在运行时检查,如果类型转换出错,程序直接GG,可 能会带来毁灭性打击。而泛型的作用就是在编译时做类型检查,这无疑培加程序的安全性

Java中的泛型通过类型擦除的方式来实现,通俗点理解,就是通过语法糖的形式,在.java->.class转换的阶段, 将ListString擦除调转为List的手段。换句话说,Java的泛型只在编译期,Jvm是不会感知到泛型的。 ing.

java中泛型中上下界限定符extends 和 super有什么区别?

? extends T表示类型的上界,表示參效化类型的可能是T或是T的子类

? super T表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至 Object

在使用 限定通配符的时候,需要遵守PECS原则,即Producer Extends, Consumer Super;上界生产,下界消费。 如果要从集合中读取类型T的数据,并且不能写入,可以使用?extends 通配符;(Producer Extends),如上面的 processNumber方法。 如果要从集合中写入类型T的数据,并且不需要读取,可以使用? super 通配符;(Consumer Super),如上面的 addElements方法。

Java中异常分哪两类,有什么区别?

Java中的异常,主要可以分为两大类,即受检异常 (checked exception)和 非受检异常(unchecked exception)

对于非受检异常来说,一般是运行时异常,继承自RuntimeException。在编写代码的时候,不需要显式的捕获, 但是如果不捕获,在运行期如果发生异常就会中断程序的执行。 开心分封 这种异常一般可以理解为是代码原因导致的。比如发生空指针、数组越界等。所以,只要代码写的没问题,这些异 常都是可以避免的。也就不需要我们显示的进行处理。

Throwable是java中最顶级的异常类,继承Object,实现了序列化接口,有两个重要的子类:Exception和 Error, 二者都是 Java 异常处理的重要子类,各自都包含大量子类。 Error和Exception的区别和联系 error表示系统级的错误,是java运行环境内部错误或者硬件问题,不能指望程序来处理这样的问题,除了退出运 行外别无选择,它是Java虚拟机抛出的。如OutOfMemoryError、StackOverflowError这两种常见的错诶都是 ERROR. exception 表示程序窝要捕捉、窝要处理的异常,是由与程序设计的不完普而出现的问题,程序必须处理的问题。 分为RuntimeException和其他异常

IndexOutOfBoundsException

finally中代码一定会执行吗?

如果没有符合这两个条件的话,finally中的代码就无法被执行,如发生以下情况,都会导致finally不会执行: 1、 System.exit()方法被执行 2、 Runtime.getRuntime().halt()方法被执行 3、 try或者catch中有死循环 4、操作系统强制杀掉了JVM进程,如执行了kill -9 5、其他原因导致的虚拟机崩溃了 6、虚拟机所运行的环境挂了,如计算机电源断了 7、如果一个finally是由守护线程执行的,那么是不保证一定能执行的,如果这时候JVM要退出,JVM会检查其他 非守护线程,如果都执行完了,那么就直接退出了。这时候finally可能就没办法执行完。

String、StringBuilder和StringBuffer的区别?

string不可变的字符串,用的final修饰

StringBuilder 内部维护了一个可变的字符数组。通过拷贝数据来实现的

StringBufferStringBuilder 继承相同的父类,但方法都添加了 synchronized 修饰:

说几种常见的语法糖?

switch 支持 String 与枚举

泛型

也就是说,对于Java虚拟机来说,他根本不认识 Map<String,String> map 这样的语法。需要在编译阶段 通过类型擦除的方式进行解语法糖。

类型擦除的主要过程如下:1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。2.移除所有的类型参数。

糖块三、自动装箱与拆箱

所以,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现 的。

糖块四、方法变长参数

枚举

当我们使用enum 来定义一个枚举类型的时候,编译器会自动帮我们创建一个 final类型的类继承 Enum 类,所以枚举类型不能被继承。

内部类

内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,outer.java 里面定义了一个内部类 inne r,一旦编译成功,就会生成两个完全不同的 .class 文件了,分别是 outer.class 和 outer$inner.cl ass。

什么是反射机制?为什么反射慢?

反射机制指的是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获 得类的所有属性和方法。 Java的反射可以:

  1. 在运行时判断任意一个对象所属的类。
  2. 在运行时判断任意一个类所具有的成员变量和方法。
  3. 在运行时任意调用一个对象的方法
  4. 在运行时构造任意一个类的对象

为什慢

1、由于反射涉及动态解析的类型,因此不能执行某些Java虚拟机优化,如JIT优化。 2、在使用反射时,参数需要包装(boxing)成Objectl]类型,但是真正方法执行的时候,又需要再拆包 (unboxing)成真正的类型,这些动作不仅消耗时间,而且过程中也会产生很多对象,对象一多就容易导致GC, GC也会导致应用变慢。 3、反射调用方法时会从方法数组中遍历查找,并且会检查可见性。这些动作都是耗时的。 4、不仅方法的可见性要做检查,参数也需要做很多额外的检查。

Java中创建对象有哪些种方式

H3-使用new关键字 的)。 这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们还可以调用任意的构造函数(无参的和有参 User user = new User(); 使用反射机制 运用反射手段,调用Java.lang.Class或者java.lang.reflect.Constructor类的nevlnstance()实例方法。 1 使用Class类的newinstance方法 可以使用Class类的newiInstance方法创建对象。这个newiInstance方法调用无参的构造函数创建对象。

使用clone方法 无论何时我们调用一个对象的clone方法,jwvm就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone 方法创建对象并不会调用任何构造函数。

什么是AIO、BIO和NIO?

BIO(BlockingV/O):同步阻基1/0,是JDK1.4之前的传统IO模型。线程发起10请求后,一直阻基,直到缓冲区 数据就绪后,再进入下一步操作。 NIO(Non-Blocking |/O):同步非阻塞I/O,线程发起I0请求后,不需要阻塞,立即返回。用户线程不原地等待 I0缓冲区,可以先做一些其他操作,只需要定时轮询检查IO缓冲区数据是否就绪即可。 AIO(Asynchronous 1/O):异步非阻塞1/0模型。线程发起I0请求后,不需要阻塞,立即返回,也不需要定时轮 询检查结果,异步IO操作之后会回调通知调用方。

jdk新特性?jdk11和jdk8有啥区别?

jdk8主要引入 lambda表达式/函数式接口,stream流/时间类型/Optional

jdk11. 引入var声明局部变量/字符串api增强/zgc垃圾回收器

Jdk17 新增密封类/文本块

jdk21 虚拟线程一种由JVM管理的轻量级线程,其创建、调度和销毁的成本远低于传统的平台线程(操作系统线程)。记录类。一种用于持有不可变数据的透明数据载体。编译器会自动生成构造器、getterequalshashCodetoString 方法。模式匹配的增强(比如switch

Lambda表达式是如何实现的?

两个lambda表达式分别调用了 Lambda$main$1 和 lambda$main$0 两个方法。

所以,lambda表达式的实现其实是依赖了一些底屋的apl,在编译阶段,编译器会把lambda表达式进行解糖,转 换成调用内部ap的方式。

JAVA并发

线程和进程的区别?

根本区别

根本区别:资源拥有的基本单位 (独立的内存、文件等),线程:CPU调度的基本单位 (共享进程资源)

资源开销:进程:大(创建、销毁、切换开销大),线程:小(可看作“轻量级进程”)

内存空间:独立,互不干扰,通信复杂,共享 其所属进程的内存,通信简单

健壮性:健壮,一个进程崩溃不影响其他进程

线程是进程的一部分,必须依赖于进程

如何快速的回收线程?

设置较短的空闲线程

使用虚拟线程(Java 21+)

主动监控和中断阻塞线程

什么是多线程中的上下文切换?

上下文切换是指 CPU 从一个线程转到另一个线程时,需要保存当前线程的上下文状态,恢复另一个线程的上下文 状态,以便于下一次恢复执行该线程时能够正确地运行。

在多线程编程中,上下文切换是一种常见的操作,上下文切换通常是指在一个 CPU 上,由于多个线程共享 CPU 时间片,当一个线程的时间片用完后,需要切换到另一个线程运行。此时需要保存当前线程的状态信息,包括程序 计数器、寄存器、栈指针等,以便下次继续执行该线程时能够恢复到正确的执行状态。同时,需要将切换到的线程 的状态信息恢复,以便于该线程能够正确运行。

在多线程中,上下文切换的开销比直接用单线程大,因为在多线程中,需要保存和恢复更多的上下文信息。过多的上下文切换会降低系统的运行效率,因此需要尽可能減少上下文切换的次数。

能不能谈谈你对线程安全的理解?

线程安全是指某个函数在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 简单来说,就是多个线程同时访问共享变量的时候,得到的结果和我们预期的一样,就是线程安全。所以有四个关 键词:并发、多线程、共享变量、正确完成。这里所谓的正确完成,其实就是要满足所谓的原子性、有序性和可见 性。

什么是并发,什么是并行?

井发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个 程序都是在同一个处理机上运行。-只有一个人在干活,微观上其实是单线程,宏观上是一起执行的

井行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两 个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。是真的有几个人同时在干活。

线程有那几种状态

Java中线程的状态分为6种:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

  2. 运行(RUNNABLE):Java线程中将就绪(READY)和运行中 (RUNNING)两种状态笼统的称为“运行”。

    就绪(READY):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配cpu使用权。

    运行中 (RUNNING):就绪(READY)的线程获得了cpu 时间片,开始执行程序代码。

  3. 阻塞(BLOCKED):表示线程阻塞于锁(关于锁,在后面章节会介绍)。

  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  6. 终止(TERMINATED):表示该线程已经执行完毕。

什么是守护线程,和普通线程有什么区别?

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)。用户线程一般用于执行用户级任 务,而守护线程也就是“后台线程”,一般用来执行后台任务,守护线程最典型的应用就是GC(垃圾回收器)。 这两种线程其实是没有什么区别的,唯一的区别就是Java虚拟机在所有<用户线程>都结束后就会退出,而不会等< 守护线程>执行完。

JDK21 中的虚拟线程是怎么回事?

在以前的JDK中,Java的线程模型其实比较简单,在大多数操作系统中,主要采用的是基于轻量级进程实现的一 对一的线程模型,简单来说就是每一个Java线程对应一个操作系统中的轻量级进程,这种线程模型中的线程创 建、析构及同步等动作,都需要进行系统调用。而系统调用则露要在用户态(User Mode)和内核态(Kernel Mode)中来回切换,所以性能开销还是很大的。 而新引入的虚拟线程,是JDK 实现的轻量级线程,他可以避免上下文切换带来的的額外耗费。他的实现原理其实 是JDK不再是每一个线程都一对一的对应一个操作系统的线程了,而是会将多个虚拟线程映射到少量操作系統线程 中,通过有效的调度来道免那些上下文切换。

创建线程有几种方式?

•继承Thread类创建线程 • 实现Runnable接口创建线程 • 通过Callable和FutureTask创建线程 • 通过线程池创建线程

run/start、wait/sleep、notify/notifyAll区别?

run方法和start方法区别 我们创建好线程之后,想要启动这个线程,则需要调用其start方法。所以,start方法是启动一个线程的入口。 如果在创建好线程之后,直接调用其run方法,那么就会在单线程中直接运行nun方法,不会起到多线程的效果。

sleep和wait区别

sleep()方法可以在任何地方使用;而wait方法则只能在同步方法或同步块中使用。

wait 方法会释放对象锁,但 sleep 方法不会。

wait的线程会进入到WAITING状态,直到被唤醒:sleep的线程会进入到TIMED_WAITING状态,等到指定时间之 后会再尝试获取CPU时间片。

因为Java锁的目标是对象,而wait需要释放锁,所以针对的目标都是对象,所以把他定义在Object类中。而 sleep()不需要释放锁,所以他针对的目标是线程,所以定义在Thread类中。

notify和notifyAIl区别 当一个线程进入wait之后,就必须等其他线程notify()/notifyAll(),才会从等待队列中被移出。

使用notifyAIl,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。

但是唤醒的这些线程只是进入争夺队列,并不表示立即就可以获得CPU开始执行,因为wait方法被调用的时候, 线程已经释放了对象锁。所以,被notify()/notifyAll()唤醒的线程,只是表示他们可以竞争锁了,觉争到锁之后才有机会被CPU调度。那么notityAIl虽然可以把所有线程都唤醒,让他们都可以竞争锁,但是最终也只有一个可以获得锁并执行。notify和notifyAll因为也是操作对象的,所以把他们定义在Object类中。

什么是死锁,如何解决?

(1) 互斥条件:一个资源每次只能被一个进程使用。 (2) 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 (3)不可强行占有:进程已获得的资源,在未使用完之前,不能强行剥夺。 (4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何解除死锁 想好解除和预防死锁,就避免4个条件同时发生就行了,一般从以下几个方面入手

  • 破坏不可抢占:设置优先级,使优先級高的可以抢占资源

  • 破坏循环等待:保证多个进程(线程)的执行顺序相同即可避免循环等待。 如执行顺序都是:A~>B->C,那就可以避免循环等待。

    最常用的避免方法就是破坏循环等待,就是当我们有多个事务的时候,最好让这几个事务的执行顺序相同。如事务1:A->B->C,事务2:C->D->A,这种情况就有可能导致死锁。 即事务1占有了A,等待C,而事务2占有了C在等待A。 所以,要避免死锁就把事务2改为:A->D->C。

什么是ThreadLocal,如何实现的?

ThreadLocal是java.lang下面的一个类,是用来解决java多线程程序中并发问题的一种途径;通过为每一个线程创 建一份共享变量的副本来保证各个线程之间的变量的访问和修改互相不影响;

ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这 样处理后,能够优雅的解决一些实际问题。 比如一次用户的页面操作请求,我们可以在最开始的flter中,把用户的信息保存在ThreadLocal中,在同一次请求 中,再使用到用户信息,就可以直接到ThreadLocal中發取就可以了。 ThreadLocal有四个方法,分别为:

  • initialValue 返回此线程局部变量的初始值
  • get 返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。
  • set 将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于
  • initialValue() 方法来设置线程局部变量的值。
  • remove 移除此线程局部变量的值。

线程同步的方式有哪些?

线程同步指的就是让多个线程之间按照顺序访问同一个共享资源,避免因为并发冲突导致的问题,主要有以下几种 方式: synchronized:Java中最基本的线程同步机制,可以修饰代码块或方法,保证同一时间只有一个线程访问该代码 块或方法,其他线程需要等待锁的释放。

ReentrantLock:与synchronized关键字类似,也可以保证同一时间只有一个线程访向共享资源,但是更灵活, 支持公平锁,可中断锁、多个条件变量等功能。

Semaphore:允许多个线程同时访问共享资源,但是限制访问的线程数量。可以用于控制并发访问的线程败量, 避免系统资源被过度占用。

CountDownLatch:允许一个或多个线程等待其他线程执行完毕之后再执行,可以用于线程之间的协调和通信。

CyclicBarrier类:允许多个线程在一个栅栏处等待,直到所有线程都到达栅栏位置之后,才会继续执行。

Phaser:与CyclicBarrier类似,也是一种多线程同步工具,但是支持更灵活的栏栅操作,可以动态地注册和注销 参与者,并可以控制各个参与者的到达和离开。

java中的synchronized是怎么实现的?

synchronized 是 Java 中的一个很重要的关键字,主要用来加锁,synchronized 所添加的锁有以下几个特点。synchronized 的使用方法比较简单,主要可以用来修饰方法和代码块。根据其锁定的对象不同,可以用来定义同步方法和同步代码块。

方法级的同步是隐式的(同步方法)。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要 访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始 执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断 住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方 法外面之前监视器锁会被自动释放。

同步代码块使用monitorenter 和 monitorexit两个指令实现。可以把执行 monitorenter指令理解为加锁,执行 monitorexit理解为释放锁。每个对象维护着一个记录着被锁次效的计数器。未被锁定的对象的该计数为 0,当一个线程获得锁(执行 monitorenter))后,该计数器自增变为1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit指令)的时候,计数器再自减。当计数器为 0的时候。锁将被释放,其他线程便可以获得锁。

synchronized是如何保证原子性、可见性、有序性的?

1.那么,synchronized如何保证的原子性呢? synchonized其实是通过 monitorenter 和 monitorexit 这两个字节码指令实现的。 当线程执行到 monitorenter 的时候要先获得锁,才能执行后面的方法。当线程执行到 monitorexit 的时候则要释 放锁。 在未释放之前,其他线程是无法再次获得锁的,所以,通过monitorenter和monitorexit指令,可以保证被 synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在 Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。 线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解 锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。而 由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代 码执行完。这就保证了原子性。

2.这里就需要把有序性的概念扩展一下了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所 有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。

3.可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 Java内存模型规定了所有的变量都存储在主内存中,每系线程还有自己的工作内存,线程的工作内存中保存了该 线程中使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内 存。 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行 数据同步进行。所以,就可能出现线程1改了某个变量的僖,但是线程2不可见的情况。

被synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是 这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的 值。

synchronized的锁升级过程是怎样的?

所以,在Java中,锁的状态分为四种,分别是无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。在Java 中,mark word的低两位用于表示锁的状态,分别为“01”(无锁状态)、“01”(偏向锁状态)、“00”《轻量级锁状 态)和“10”(重量级锁状态)。但是由于无锁状态和偏向锁都是“O1”,所以在低三位引入偏向锁标记位,用”0”表示=无锁,“1表示偏向。

当一个synchronized块被统程首次进入时,锁对象会进入偏向模式。 在偏向锁模式下,锁会偏向于第一个获取它的线程,JVM 会在对象头中记录该线程的ID 作为偏向锁的持有者,并将对象头中的 Mark Word 中的一部分作为偏向锁标识。

在这种情况下,如果其他线程访问该对象,会先检查该对象的偏向锁标识,如果和自己的线程ID 相同,则直接获取锁。如果不同,则该对象的锁状态就会升级到轻量级锁状态。

轻量级锁(Lightweight Locking)

当有另一个线程尝试获取已被傧向的锁时,偏向锁会被撤销,锁会升级为轻量级锁。 在轻量级锁状态中,JVM为对象头中的 Mark Word 预留了一部分空间,用于存储指向线程栈中锁记录的指针。

当一个线程尝试获取轻量级锁时,JVM的做法是:

  1. 将对象头中的Mark Word复制到线程栈中的锁记录(Lock Record):每个Java对象头部都有一个Mark Word,它用于存储对象自身的运行时数据,如哈希码、锁状态信息、代年龄等。当线程尝试获取轻量级锁 时,JVM会在当前线程的栈帧中创建一个锁记录空间,然后将对象头中的Mark Word复制到这个锁记录中。 这个复制的Mark Word被称为"Displaced Mark Word”。
  2. 尝试通过CAS操作更新对象头的MarkWord:接下来,JVM尝试使用CAS(Compare-And-Swap)操作,将 对象头的Mark Word更新为指向锁记录的指针。如果这个更新操作成功,那么这个线程就成功获取了这个对 象的轻量级锁。如果替换成功,则该线程获取锁成功;如果失败,则表示已经有其他线程获取了锁,则该锁状态就会升级到重量级锁状态。 触发条件:当有另一个线程尝试获取已被偏向的锁时,偏向锁会升级为轻量级锁。

当轻量级锁的CAS自旋多次失败,锁会进一步膨胀为重量级锁。 当锁状态升级到重量级锁时,JVM 会将对象头中的 Mark Word 修改为指向一个重量级锁结构(Monitor),该 结构包含一个 Entry Set,用于管理那些尝试获取锁但暂时无法获得的线程。 Entry Set 是重量级锁(Monitor])的一个部分,用来存放那些 尝试获取锁但未成功 的线程。这些线程处 于 BLOCKED 状态。当持有锁的线程释放锁时,JVM 会从 Entry Set中选出一个线程来竞争锁。

此时,如果一个线程想要获取该对象的锁(当前对象已被其他线程锁定时),则需要先进入等待队列,等待该锁被 释放。当锁被释放时,JNM 会从等待队列中选择一个线程唤醒,并将该线程的状态设置为“就绪”状态,然后等待 该线程重新获取该对象的锁。

触发条件:当轻量级锁的CAS操作失败,轻量级锁升级为重量级锁。

什么是volatile?

volatile关键字通过底层CPU的内存屏障机制,保证了变量的可见性有序性

1保证可见性。问题:在多核CPU环境下,每个线程可能会在自己的工作内存(如CPU缓存)中操作变量。如果一个线程修改了变量,可能不会立即写回主内存,导致其他线程读取到的还是旧值。解决:当一个 volatile`变量被修改后,JVM会确保新值能立即被强制刷新到主内存。并且,当其他线程要读取这个变量时,它会强制从主内存重新拉取最新的值。这样就保证了所有线程看到的这个变量的值都是一致的。

2.禁止指令重排序:问题:为了提升执行效率,编译器和处理器常常会对指令进行重排序。但在多线程环境下,这种重排序可能会导致程序出现意想不到的错误。解决:volatile通过插入内存屏障 来禁止处理器对 volatile变量读写操作的指令进行重排序,从而确保代码的执行顺序符合我们的预期。

volatile 的实现原理,底层是依赖于处理器的 内存屏障 指令来实现的。内存屏障就像一个“栅栏”,让屏障前后的指令不能随意交换位置。

具体来说,JVM 在读写 volatile 变量时会插入以下屏障:

  • 写操作时
    • 在每个 volatile 写操作之前,插入一个 StoreStore 屏障,确保前面的所有普通写操作都已经对其他处理器可见。
    • 在每个 volatile 写操作之后,插入一个 StoreLoad 屏障,强制将写缓冲区的数据刷新到主内存,并让其他处理器的缓存失效。
  • 读操作时
    • 在每个 volatile 读操作之后,插入一个 LoadLoad 屏障和一个 LoadStore 屏障,确保后面的所有读/写操作都必须在这个 volatile 变量加载完成后才能开始。

volatile能保证原子性吗?为什么?

volatile通常被比喻成”轻量级的synchronized”,也是Java并发編程中比较重要的一个关键字。和synchronized不 同,volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。

volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。 但是,volatile在线程安全方面,可以保证有序性和可见性,但是是不能保证原子性的。

synchronized可以保证原子性,因为被synchronized修饰的代码片段,在进入之前加了锁,只要他没执行究,其 他线程是无法获得锁执行这段代码片段的,就可以保证他内部的代码可以全部被执行。进而保证原子性。

那么,为什么volatile不能保证原子性呢?因为他不是锁,他没做任何可以保证原子性的处理。当然就不能保证原 子性了。

为什么JDK 15要废弃偏向锁?

在过去,Java 应用通常使用的都是 HashTable、Vector 等比较老的集合库,这类集合库大量使用了 synchronized 来保证线程安全。所以偏向锁技术作为synchronized的一种优化手段,可以减少无锁竞争情况下的 开销,通过假定一个锁一直由同一线程拥有,从而道免执行比较和交换的原子操作。

但是,偏向锁的局限是当只有一个线程反复进入同步代码块时他才能快速获得,但是当有其他线程尝试获取锁的时 候,就需要等到 safe point时,再将偏向锁撤销为无锁的状态或者升级为轻量级锁,而这个过程其实是会消耗一 定的性能的。

总之,废弃偏向锁是为了减少复杂性、提高代码可维护性,井鼓勋开发人员采用更现代的并发编程技术,以适应当 今Java应用程序的性能需求。

java并发-什么是AQS?

AQS抽象队列同步器,我的理解是它是Java并发包中一个最核心、最基础的理论框架。很多我们日常使用的同步工具,比如ReentrantLock、CountDownLatch、Semaphore等,其内部实现都依赖于AQS。这个其实主要有几种考量,

锁的互斥性,可重入性,锁的性能(并发高不高,锁竞争是否严重),避免死锁

AQS的核心思想是,它通过一个整型的volatile变量(名为state 来表示同步状态,并配合一个内置的FIFO双向队列(CLH队列的变体) 来完成资源获取线程的排队工作。

AQS队列其实是一种MPSC。多生产者单消费者模式。多个线程都可以尝试获取锁,并加入队列尾部,好处就是直接往后添加,并发好,单消费者通常情况下,只有一个线程(即持有锁并即将释放它的那个线程,从队列头部删除节点,并唤醒下一个线程)。

状态(state):这是一个关键字段,不同的同步器对它有不同的诠释。比如:

  • 在ReentrantLock中,state表示锁的持有计数(0表示未锁定,>0表示被锁定,且可重入)。
  • 在Semaphore中,state表示当前可用的许可证数量。
  • 在CountDownLatch中,state表示计数器当前的值。

什么是CAS?存在什么问题?

CAS是一项乐观锁技术,是Compare And Swap的简称,顾名思义就是先比较再替换。CAS 操作包含三个操作数 一内存位置(V)、预期原值(A)和新值(B)。在进行并发修改的时候,会先比较A和V中取出的值是否相等,如 果相等,则会把值替换成B,否则就不做任何操作。 当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败 自 的线程并不会被挂起,而是被告知这次竞争中失败,并可以次尝试。 在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于synchronized这种阻塞算法,CAS是 非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。 CAS的主要应用就是实现乐观锁和锁自旋。

会导致aba的问题,忙等待

CAS一定有自旋吗?

不一定,但是通常为了提高CAS的成功率,会考虑做自旋。 最简单的自旋就是while(true) 通常情况下,CAS 操作都会采用自旋的方式,当 CAS 失败时,会重新尝试执行 CAS 操作,直到操作成功或达到 最大重试次数为止。 因为,CAS 操作一股都是在多线程并发访问时使用,如果直接阻塞线程,会导致性能下降,而采用自旋的方式, 可以让 CPU 空转一段时间,等待锁被释放,从而避免线程切换和阻塞的开销。

什么是Unsafe?

Unsafe是CAS的核心类。因为Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管 如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。

Unsafe是Java中一个底层类,包含了很多基础的操作,比如数组操作、对象操作、内存操作、CAS操作、线程 (park)操作、栅栏(Fence)操作,JUC包、一些三方框架都使用Unsafe类来保证并发安全。

Unsafe类在jdk 源码的多个类中用到,这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效 率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是Unsafe的,它所分配的内存需要手动free(不被 GC回收)。Unsafe类,提供了JNI某些功能的简单替代:确保高效性的同时,使事情变得更简单。

Unsafe类提供了硬件级别的原子操作,主要提供了以下功能: 1、通过Unsafe类可以分配内存,可以释放内存; 2、可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的; 3、将线程进行挂起与恢复 4、CAS操作

CAS在操作系统层面是如何保证原子性的?

CAS是一种基本的原子操作,用于解决并发问题。在操作系统层面,CAS 操作的原理是基于硬件提供的原子操作 指令。在x86架构的CPU中,CAS 操作通常使用 cmpxchg 指令实现。 为啥cmpxchg指令可以保证原子性呢?主要由以下几个方面的保障:

  1. cmpxchg指令是一条原子指令。在CPU执行 cmpxchg指令时,处理器会自动锁定总线,防止其他CPU 访问共享变量,然后执行比较和交换操作,最后释放总线。

  2. cmpxchg指令在执行期间,CPU会自动禁止中断。这样可以确保 CAS 操作的原子性,避免中断或其他干扰 对操作的影响。

  3. cmpxchg 指令是硬件实现的,可以保证其原子性和正确性。CPU 中的硬件电路确保了 cmpxchg指令的正确 执行,以及对共享变量的访问是原子的。

synchronized和reentrantLock区别?

ReentrantLock 和 synchronized 都是用于线程的同步控制,但它们在功能上来说差别还是很大的。对比下来 ReentrantLock 功能明显要丰富的多。 二者相同点是,都是可重入锁。二者也有很多不同,如:

synchronized是Java内置特性,而ReentrantLock是通过Java代码实现的。

synchronized是可以自动获取/释放锁的,但是ReentrantLock黑要手动获取/释放锁。

ReentrantLock还具有响应中断、超时等待等特性。

ReentrantLock可以实现公平锁和非公平锁,而synchronized只是非公平锁。

公平锁和非公平锁的区别?

非公平锁:多个线程不按照申请锁的顺序去获得锁,而是直接去尝试获取锁,获取不到,再进入队列等待,如 果能获取到,就直接获取到锁。

公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得 到锁。

父子线程之间怎么共享/传递数据?

当我们在同一个线程中,想要共享变量的话,是可以直接使用ThreadLocal的,但是如果在父子线程之间,共享变 量,ThreadLocal就不行了。

手工复制

与 ThreadLocal 不同,InheritableThreadLocal 可以在子线程中继承父线程中的值。在创建子线程时,子线程将 复制父线程中的 InheritableThreadLocal变量。

CompletableFuture的底层是如何实现的?

CompletableFuture是Java 8中引入的一个新特性,它提供了一种简单的方法来实现异步编程和任务组合。他的底 层实现主要涉及到了几个重要的技术手段,如Completion链式异步处理、事件驱动、ForkJoinPool线程池、以及 CountDownLatch控制计算状态、通过CompletionException捕获异常等。 CompletableFuture 内部采用了一种链式的结构来处理异步计算的结果,每个 CompletableFuture 都有一个与之 关联的 Completion 链,它可以包含多个 Completion 阶段,每个阶段都代表一个异步澡作,并且可以指定它所依 赖的前一个阶段的计算结果。(在 CompletableFuture 类中,定义了一个内部类 Completion,它表示 Completion 链的一个阶段,其中包含了前一个阶段的计算结果、下一个阶段的计算操作以及执行计算操作的线程 池等信息。 CompletableFuture 还使用了一种事件驱动的机制来处理异步计算的完成事件。在一个 CompletableFuture 对象 上注册的 Completion 阶段完成后,它会触发一个完成事件,然后 CompletableFuture 对象会执行与之关联的下 一个 Completion 阶段。 CompletableFuture 的异步计算是通过线程池来实现的。CompletableFuture在内部使用了一个ForkJoinPool线 程池来执行异步任务。当我们创建一个CompletableFuture对象时,它会在内部创建一个任务,并提交到 ForkJoinPool中去执行。

如何实现主线程捕获子线程异常

如果想要在主线程能够捕获子线程的异常,可以考虑使用Callable和Future,它们允许主线程获取子线程的执行结 果和异常。这样,主线程可以检查子线程是否抛出了异常,并在必要时处理它。以下是一个示例:

uncaughtexceptionhanaler 我们还可以使用 UncaughtExceptionHandler), UncaughtExceptionHandler是 Java 中的一个接 口,用于处理未捕获异常,即那些没有被 try-catch 块捕获的异常。它允许定义自定义异常处理器,以便在线程 出现未捕获异常时采取特定的操作。 有了它,我们就可以为线程设置一个自定义的未捕获异常处理器,当线程抛出未捕获异常时,该处理器会被调用, 我们可以在其中记录异常信息、执行清理操作等。

为什么不能在try-catch中捕获子线程的异常?

在Java中,主线程不能直接捕获子线程抛出的异常的!主要是因为子线程和主线程是独立的执行单元,它们的执 行是并发的,因此主线程天法捕获子线程的异常。子线程的异常通常由子线程自己处理或通过适当的异常处理机制 处理。 线程隔离,这也是Java保证线程出现异常不会影响整个进程的一个主要原理。

什么是happens-before原则?

happens-before原则是一种用于描述多线程程序中操作执行顺序的规则。它是Java内存模型 (Java Memory Model,JMM)的一部分:如果一个操作 A “happen-before”另一个操作 B,那么A 的结果对B 是可见的。这 个概念是理解线程间内存可见性的关键。

ThreadLocal为什么会导致内存泄漏?如何解决的?

会导致ThreadLocal内存泄漏的部分其实就是他在堆上存储的ThreadLocalMap中的K-V部分:

key是弱引用,下次gc就直接回收,但是value不是

所以,当我们在一个ThreadLocal用完之后,手动调用一下remove,就可以在下一次GC的时候,把Entry清理掉。

AQS的同步队列和条件队列原理?

同步队列和条件队列是AQS中的两种不同队列,同步队列主要用于实现锁机制,而条件队列用于线程间的协调和 通信。

同步队列主要用于实现锁的获取和释放。如我们常用的ReentrantL.ock,就是基于同步队列来实现的。 我们在介绍AQS的时候介绍过,它是一个FFO队列,节点类型为AQS内部的Node类。当一个线程尝试获取锁失败 时,它会被封装成一个Node节点加入到队列的尾部(每个节点(Node)代表一个等待的线程)。当锁被释放时, 头节点的线程会被唤醒,尝试再次获取锁。

•当一个线程尝试获取锁并失败时,AQS会将该线程包装成一个节点(Node)并加入到队列的尾部。

什么是AQS的独占模式和共享模式?

它提供了一套基于FIFO队列的同步舒框架,并支持独占模式和共享模式,这两种模式是用于实现同步组件的关 键。

独占模式意味着一次只有一个线程可以获取同步状态。这种模式通常用于实现互斥锁,如ReentrantLock。

共享模式允许多个线程同时获取同步状态。这种模式通常用于实现如信号量(Semaphore)和读写锁 (ReadWriteLock的读锁)等同步组件。

AQS的各种实现类中,要么是基于独占模式实现的,要么是基于共享模式实现的。

在AQS中提供了很多和锁操作相关的方法,如:tryAcquire, tyRelease, acquire, release。tryAcquireShared, tryReleaseShared, releaseShared, acquireShared。

当需要保证某个资源或一段代码在同一时间内只能被一个线程访问时,独占模式是最合适的选择。如我们经常用的 自 ReentrantLock和ReadWriteLock中的写锁。

当资源或数据主要被多个线程读取,而写操作相对较少时,共享模式能够提高并发性能。如我们经常使用的Semaphore和CountDownLatch,用来多个线程控制共享资源的。还有ReadWriteLock中的读锁允许多个线程同 时读取数据,只要没有线程在写入数据。

为什么虚拟线程不能用synchronized?

Java中的虛拟线程旨在提高并发编程的效率和性能,通过允许创建数以百万计的线程而对系统资源的消耗最小。 虚拟线程背后的主要思想是将线程的调度任务从操作系统级别转移到Java虚拟机(JVM)级别,从而波少上下文 切换的成本并提高系统的吞吐量。

虚拟线程的主要优势之一是它们能够在执行阻塞操作时被挂起,从而释放底层的操作系统线程给其他任务使用。这 种机制极大地提升了并发处理的能力和系统资源的利用率。当虚拟线程被PINNED时,它就会一直占用一个底层线 程,即使执行阻塞操作也无法被挂起,从而限制了其他虚拟线程的执行和系统的整体可伸缩性。 而且,虚拟线程设计的一个核心目标是提高系统执行大量并发任务的能力,特别是1/0密集型任务。固定虚拟线程 到底层线程总味着减少了虚拟机能够灵活调度和优化任务执行的能力,因此降低了资源利用效率。 所以,我们需要尽可能的避免PINNED的发生。 而根据JEP425的描述,执行synchronized块或方法时,会发生PINNED,synchronized关键字涉及获取和释放内 部监视器锁,因为这种锁机制依赖于操作系统级别的同步原语,执行synchronized块或方法的虛拟线程需要固定 到一个底层线程上,以保证锁的正确管理,直到完成。

为什么虚拟线程不要和线程池一起用?

主要原因是:像所有资源池一样,线程池旨在共享昂贵的资源,但虚拟线程并不昂贵,因此永运不需要将它们池 化。 虚拟线程被设计为可以轻松地创建和销毁的轻量级实体,旨在允许每个任务都在其自己的虚拟线程中运行。这与传 统的平台线程不同,后者创建和销毁的成本较高,因此经常被放入线程池中重用以提高效率。

还有就是,虚拟线程的引入有个目的是为了简化并发编程模型,让开发者可以像編写顺序代码一样编写并发代码,回 而无需担心线程管理和线程池的复杂性。要求虚拟线程不被池化,是为了鼓励开发者利用这一简化的模型,避免回 到传统的线程池管理模式。 总之,我们可以随意利用虚拟线程的轻量级特性和系統资源的高效利用,简化并发编程模型,而无需依賴传統的线 程池技术。

为什么虚拟线程尽量避免使用ThreadLocal

虚拟线程是支持ThreadLocal的,但由于可能创建的虚拟线程数量巨大,不当使用ThreadLocal可能导致内存泄洞 等问题。如果需要,可以考虑使用作用域局部变量(Scope-local variables)作为替代方案。

如何实现无锁化编程?

所谓无锁化编程是指在多线程环境下避免使用传统的锁(如 synchronized 或 ReentrantLock),从而减 少由于锁竞争所带来的性能开销。 但是需要注意,无锁化维程并非完全不使用锁,而是指通过原子操作保证线程之间的数据一致性和操作的原子性, 而不需要显式的加锁和解锁操作。原子操作是指对莱个共享变量的操作(如加法、减法、比较等)是不可分割的, 不会被中断。 在Java中,无锁化编程通常依款于 原子操作 和 CAS机制。这些技术是硬件级别提供的支持,确保对共享数据的 修改是不可中断的。

比如AtomicInteger

无锁编程的好处 1.减少上下文切换:传统的锁在竞争时会引起线程上下文切换,导致性能下降。无锁化编程通过避免线程阻塞来 减少这些开销.

2.提高井发性:通过原子操作,多线程可以同时对共享资源进行操作,从而提高系镜的并发性。

避免死锁:无锁编程避免了锁的嵌套和资源争用,从而避免了死锁的发生。

介绍下JUC,都有哪些工具类?

Java 的 JUC (java.util.concurrent)是 Java 并发编程的核心工具包,几乎所有高性能、多线程的 Java 应用都会用到它。从JDK 1.5 开始引入,由 Doug Lea 主导开发。

在 JUC 出现之前,Java 开发者主要依赖 synchronized 和 Object 的 wait()/notify()等方式进行线程之间的同步(保证并发安全)。但是有很多缺点:

1.粒度粗:synchronized 锁住的是整个对象或类,容易导致不必要的线程阻塞,降低并发性能。 2.功能有限:难以实现复杂的同步需求,如读写锁、信号量、屏障等。 3.易出错:手动管理 wait()和 notify()容易导致死锁等问题,代码难以编写和维护。

性能瓶颈:synchronized 是重量级锁,他的性能可能成为瓶颈。

于是就出现了JUC,说白了就是提供了很多并发安全的工具类,帮助开发者来在并发场景下使用。JUC中 的工具类会通过细粒度锁、CAS、并发数据结构等大幅提升高并发场景下的吞吐量和响应速度。

这部分是 JUC 中最常用的一部分,统一了线程的创建、调度和销毁。

Executor:执行任务的最顶层接口(任务用 Runnable 表示)。

ExecutorService:扩展了Executor,提供了生命周期管理(shutdown)和任务提交(submit)等方法。 ScheduledExecutorService:支持延时与周期性任务调度。

主要实现类

ThreadPoolExecutor:可配置的线程池,支持核心线程数、最大线程数、队列、拒绝策略等。ScheduledThreadPoolExecutor:定时任务线程池。工具类 Executors:提供常用线程池工厂方法(但生产环境推荐自己用 ThreadPoolExecutor构造,避免资源问题)。

ForkJoinPool /ForkJoinTask:分而治之的并行计算框架,

新特性 • JDK 8引入 CompletableFuture:支持链式异步编程。

JDK 21 可结合虚拟线程 Executors.newVirtualThreadPerTaskExecutor()。

同步器/以及并发集合

JDK25的ScopedValue是什么?为什么可以替代ThreadLocal?

ScopedValue 是 Java 25 中引入的一个API (JEP 429),它是一种能够在特定作用域内共享不可变数据的新 机制,主要用于在线程内和跨线程之间安全、高效地传递数据。

它的核心思想是“作用城”(Scope)。一个ScopedValue的值被綁定到一个动态定义的作用城上。一旦执行流 程退出这个作用城,该綁定就会被自动撤销。这个作用城通常由 runwhere(ScopedValue, value, Runnab le)方法来定义。

解决内存泄漏问题

ThreadLocal你需要手动管理其生命周期。调用 set(value)后,你必须记得在 finally 块中调用remove(),否则数据会一直留在线程中,导致内存泄漏(尤其是在线程池中,线程会被复用).

ScopedValue的生命周期严格限定在 runwhere方法定义的作用域内。退出作用域后,绑定自动地被 清除,无需手动移除,从根本上避免了内存泄漏的风险。

虚拟线程更友好

ThreadLocal的内部使用线程内部的 ThreadLocalMap,也就总味着每个 Thread 内部维护了一个ThreadLocalMap..

对于平台线程,因为不会太多,所以还可以接受,但是对于虚拟线程,那可能是成千上万的,那么有这么多个Map,就显得笨重了。

ScopedValue 的值不存储在线程中,而是存储在作用城中。一个 ScopedValue 的绑定在其作用城内(即runwhere 方法定义的代码块内)对所有在该作用城中运行的代码(包括所有子任务)都是可见的。

父子线程间传递更友好

ThreadLocal本身不支持主子线程之间的传递,需要借助InheritableThreadLocal实现

在一个 ScopedValue.runwhere 作用城内,使用 StructuredTaskScope. fork()(结构化并 发,这个特性在JDK25中还是预览版)创建的所有子任务(虚拟线程)自动就能够读取到父作用城中绑定的 ScopedValue。

性能更好

ThreadLocal内部使用线程内部的 ThreadLocalMap,这是一个哈希表结构。get()和 set()操作涉及哈希查找,虽然很快,但在极端高性能场景下仍有开销。

JVM 可以将 ScopedValue 的读取优化为类似局部变量访问的速度,因为它基于不可变且范围限定的数 据。

集合类

java集合中为什么HashMap的扩容是2的倍数

首先,查找数据位置快,使用位运算,代替了取模的算法

第二个,扩容的时候,同一个节点的链表,只会被扩容2个链表

ConcurrentHashMap是如何保证线程安全的?

在JDK 1.7中,ConcurrentHashMap使用了分段锁技术,即将哈希表分成多个段,每个段拥有一个独立的锁。这 样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。

虽然JDK 1.7的这种方式可以减少锁竞争,但是在高并发场景下,仍然会出现锁竞争,从而导致性能下降。

在JDK 1.8中,ConcurrentHashMap的实现方式进行了改进,使用节点锁的思想,即采用“CAS+Synchronized”的机制来保证线程安全。

在JDK 1.8中,ConcurrentHashMap会在添加元素时,使用CAS无锁初始化桶和将元素插入空桶;针对已有节点的桶,使用synchronized加锁,再次尝试put。这样可以避免分段锁机制下的锁粒度太大,以及在高井发场景 下,由于线程数量过多导致的锁竞争问题,提高了并发性能。

JVM

java是如何实现平台无关的?

平台无关性就是一种语言在计算机上的运行不受平台的约束,一次编译,到处执行(Write Once,Pun Anywhere).

不同的硬件和操作系统,指令不一样。这些工作是我们的虚拟机完成的,java语言是平台无关的,但是jvm是平台相关的。

java是编译型还是解释型

我们常用的编程语言,比如C语言、Java、Python、Go等都是高级语言,想要把高级语言转变成计算机认识的机 器语言有两种方式,分别是编译和解释。

通常认为编译的过程就是通过编译器(compiler)把高级语言的源代码,直接编译成可以被机器执行的机器码,交 由机器执行。如C语言。而解释的过程就是通过解释器(interpreter)直接解释执行,不需要编泽成机器语言。如JavaScript。

所以,Java语言不能简单的划分成编译型或者解释型,如果非要定义的话,那只能认为,他既是编译型、又是解 释型。正常的代码是解释执行的,JIT优化的过程是编译执行的。

JVM运行时内存区域是什么样子的

根据Java虚拟机规范的定义,JVM的运行时内存区域主要由Java堆、虚拟机栈、本地方法栈、方法区和程序计数 器以及运行时常量池组成。其中堆、方法区以及运行时常量池是线程之间共享的区域,而栈(本地方法栈+虚拟机 栈)、程序计数器都是线程独享的。

  • 程序计数器:一个只读的存储器,用于记录Java虚拟机正在执行的字节码指令的地址。它是线程私有的,为每个线程维护一个独立的程序计数器,用于指示下一条将要被执行的字节码指令的位置。它保证线程执行一个字节码指令以后,才会去执行下一个字节码指令。
  • Java虚拟机栈:一种线程私有的存储器,用于存储Java中的局部变量。根据Java虚拟机规范,每次方法调用都会创建一个栈唤,该栈领用于存储局部变量,操作数栈,动态链接,方法出口等信息。当方法执行完毕之后,这个栈帧就会被弹出,变量作用域就会结束,数据就会从钱中消失。
  • 本地方法栈:本地方法栈是一种特殊的栈,它与Java虚拟机栈有相同的功能,但是它支持本地代码(Native Code)的执行。本地方法栈中存放本地方法(Native Method)的参数和局部变量,以及其他一些附加信总。 这些本地方法一般是用C等本地语言实现的,虚拟机在执行这些方法时就会通过本地方法栈来调用这些本地方法。
  • Java堆:是存储对線实例的运行时内存区域。它是虚拟机运行时的内存总体的最大的一块,也一直占据者虚拟机内存总量的一大部分。Java堆由Java虚拟机管理,用于存放对象实例,是几乎所有的对象实例都要在上面分配内存。此外,Java堆还用于垃圾回收,虚拟机发现没有被引用的对象时,就会对堆中对象进行垃圾回收,以释放内存空间。
  • 方法区:用于存储已被加载的类信息、常量、静态变量、即时编译后的代码等数帮的内存区域。每加载一个类,方法区就会分配一定的内存空间,用于存储该类的相关信息,这部分空间随着需要而动态变化。方法区的具体实现形式可以有多种,比如堆、永久代、元空间等。
  • 运行时常量池:是方法区的一部分。用于存储编译阶段生成的信息,主要有字面量和符号引用常量两类。其中符号引用常量包括了类的全限定名称、宇段的名称和描述符、方法的名称和描述符。

java中的对象一定在堆上分配空间吗?

不一定,在HotSpot虚拟机中,存在JIT优化的机制,JIT优化中可能会进行遠逸分析,当经过透逸分析发现某一个 局部对象没有逃逸到线程和方法外的话,那么这个对象就可能不会在堆上分配内存,而是进行栈上分配。

java中的堆是如何进行分代的?

Java的堆由新生代(Young Generation)和老年代(Old Generation)组成。新生代存放新分配的对象,老年 代存放长期存在的对線。 新生代(Young)由年轻区(Eden)、Survivor区组成 (From Survivor、To Survivor)。默认情况下,新生代的 Eden区和Survivor区的空间大小比例是8:2,可以通过-XX:SurvivorRatio参数调整。新生代和老年代空间比例是1:2

java新生代如果只有一个Eden+一个Survivor可以吗?

答案是不行,如果只有两个区域,也能实现复制算法,但是会大大浪费空间。这个主要跟复制算法相关

因为新生代主要使用的是 标记-复制 算法进行垃圾回收的。刚开始对象都分配在Eden区,如果Eden区快满了就 触发垃圾回收,把Eden区中的存活对象转移到一块空着的survivor区,eden区清空,然后再次分配新对象到eden 区,再触发垃圾回收,就把eden区存活的和survivor区存活的转移到另一块空着的survivor。 那么也就是说,在平常的时候,新生代的区域中是只有一块eden和一块survivor区在被使用的,而另一块Survivor 区是空着的,所以内存使用率大约90%。

如果只有一个eden或者一个survivor区域的话,要么换垃圾回收算法,要么就是选择区域承担新对象的分配工作.

YoungGC和FullGC的触发条件是什么?

  • YoungGC的触发条件比较简单,那就是当年轻代中的eden区分配满的时候就会触发。

  • FullGC的触发条件比较复杂也比较多,主要以下几种:

    • 老年代空间不足

      创建一个大对象,超过指定阈值会直接保存在老年代当中,如果老年代空间也不足,会触发Full GC。YoungGC之后,发现要移到老年代的对象,老年代存不下的时候,会触发一次FullGC

    • 空间分配担保失败

      当准备要触发一次YoungGC时,会进行空间分配担保,在担保过程中,发现虚拟机会检查老年代最大可用的连续空间小于新生代所有对象的总空间,但是HandlePromotionFallure=false,那么就会触发一次 FullGC(HandlePromotionFailure 这个配置,在JDK 7中并不在支持了,这一步骤在该版本已取消)

      当准备要触发一次YoungGC时,会进行空间分配担保,在担保过程中,发现虚拟机会检查老年代最大可 用的连续空间小于新生代所有对象的总空间,但是HandlePromotionFailure=true,继续检查发现老年代 最大可用连续空间小于历次晋升到老年代的对象的平均大小时,会触发一次FullGC

    • 永久代空间不足。如果有永久代的话,当在永久代分配空间时没有足够空间的时候,会触发FullGC
    • 代码中执行System.gcl) 代码中执行System.gcl)的时候,会触发FulIGC,但是并不保证一定会立即触发。

什么是Stop The World?

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。这 是Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以热行,但不能与JVM交互。不管选择哪种GC算法,stop-the-world都是不能彻底避免的,只能尽量降低STW的时长。为什么需要STW呢?

  • 首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。

  • 其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和多标。

    多标:其实就是这个对象原本应该被回收棉的垃圾对象,但是被错误的标记成了存活对象。从而导致这个对象没有回被GC回收掉。这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。 垃圾收集在标记的过程中,其实标记的并不是垃圾对象,而是通过可达性分析标记可达对象。所以当一个对象本来是垃圾对象(不可达对象),但是错误的标记成非垃圾对象(可达对象)时,就是多标了。就会导致浮动垃圾。

    漏标:一个对象本来应该是存活对象,但是没有被正确的标记上,导致被错误的垃圾回收掉了。

JVM有哪些垃圾回收算法?

垃圾回收算法指的是JVM用什么样的机制来做垃圾的回收。典型的回收算法有 标记-清除算法)、复制算 法、标记-整理算法。

1.标记-清除就是将回收过程分为标记、清除两个阶段。标记:MGC Roots 对象开始,遍历整个对象图,标记所有存活的对象。(注意,标记的是存活对象,不是垃圾对象)

清除:遍历整个堆内存,回收所有未被标记的(即死亡的)对象所占用的空间。

缺点就是:会产生内存碎片,造成内存的浪费。

2.标记-复制算法的工作原理是这样子的:首先将内存划分成两个区城。新创建的对象都放在其中一块内存上面,当快满的时候,就将标记出来的存活的对象复制到另一块内存区域中(注意:这些对象在复制的时候其内存空间上是严格排序且连续的),这样就腾出来的那一半就又变成了空闲空间了。依次循环运行。

缺点:浪费了一半的内存空间,复制对象会造成性能和时间上的消耗。

3.标记整理:

标记:它的第一个阶段与标记-清除算法是一模一样的,均是避历 GC Roots,然后将存活的对象标记。 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第 二阶段才称为堅理阶段。

缺点:比较太耗时间。

常见的垃圾回收器有哪些?

常见的垃圾回收器如下:

  1. 串行垃圾回收器(Serial Garbage Collector)如:Serial GC, Serial Old
  2. 并行垃圾回收器(Parallel Garbage Collector) 如:Parallel Scavenge, Parallel Old, ParNew
  3. 并发标记扫描垃圾回收器(CMS Garbage Collector)
  4. G1垃圾回收器(G1 Garbage Collector, JDK 7中推出,JDK 9中设置为默认)
  5. ZGC垃圾回收器(The Z Garbage Collector, JDK 11 推出)

新生代收集器有Serial、ParNew、Parallel Scavenge;

老年代收集器有Serial Old、Parallel Old、CMS.

整堆收集器有G1、ZGC

JVM如何判断对象存活?

JVM有两种算法来判断对象是否存活,分别是引用计数法和可达性分析算法

  • 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就 减1;任何时候计数器为 0的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚找 机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 循环引用会导致对象无法被回收,最终会导致内存泄漏及内存溢出
  • 可达性分析算法:这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开 始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此 对象是不可用的。 但是,并不是说当进行完可达性分析算法后,即可证明某对象可以被GC。对象是否存活,需要两次标记:
    1. 第一次标记通过可达性分析算法。如果没有GC Roots相连接的引用链,那么将第一次标记
    2. 如果对象的 finalize()方法被覆盖并且没有执行过,则放在F-Queue队列中等待执行(不一定会执行),如果一段时间后该对象的 finalize()方法被执行且和GC Roots关联,则移出“即将回收”集合。如果仍然没有关联,则进行第二次标记,才会对该对象进行回收。
    3. 缺点就是stw时间长
  • 三色标记算法

jvm-GCRoots中的对象

。 虚拟机栈(栈帧中的局部变量表)中引用的对象。 • 方法区中类静态属性引用的对象。 。 方法区中常量引用的对象。 。 本地方法栈中JNI(即Native方法)引用的对象。 。 Java虚拟机內部的引用(如基本数据类型对应的Class对象,常驻的异常对象等)。 。 被同步锁(synchronized关键字)持有的对象。

什么是三色标记算法

三色标记法将对象分为三种状态:白色、灰色和黑色。 白色:该对象没有被标记过。在垃圾回收周期开始时,所有对象都是白色。这代表着它们是潜在的垃圾 灰色:该对象已经被标记过了,但该对象的引用的对象还没标记完。 黑色:该对象已经被标记过了,并且他的全部引用对象也都标记完了。

三色标记法的标记过程可以分为三个阶段:初始标记 《Initial Marking)、并发标记 (Concurrent Marking)和重 新标记(Remark)。

  • 初始标记:所有对象初始都是白色。遍历所有的根对象,将根对象和直接引用的对象标记为灰色。在这个阶 段中,垃圾回收器只会扫描被直接或者间接引用的对象,而不会扫描整个堆。因此,初始标记阶段的时间比较 短(Stop The World)
  • 并发标记:在这个过程中,垃圾回收器会从灰色对象开始遍历整个对象图,将被引用的对象标记为灰色,井将 已经遍历过的对象标记为黑色。并发标记过程中,应用程序线程可能会修改对線图,因此垃圾回收器需要使用 写屏障 (MWrite Barrier)技术来保证并发标记的正确性。(不需要STW)
  • 重新标记:重新标记的主要作用是标记在并发标记阶段中被修改的对象以及未被追历到的对象。这个过程中, 垃圾回收器会从灰色对象重新开始追历对象图,将被引用的对象标记为灰色,井将已经遍历过的对象标记为黑色(Stop The World)

什么是强引用/软引用

在Java中,强引用、软引用、弱引用和虚引用是用于管理对象生命周期的不同类型的引用。它们的主要作用是帮 助垃圾回收器(GC)决定何时回收对象,从而更高效地管理内存。

强引用是默认的引用方式,我们创建的new 一个对象,就是用的强引用,即如果引用一直在,就不会被 GC 回收 掉。

软引用是一种相对较弱的引用类型,用于表示对对象的“非强”引用。SoftReference。LocalCache类用到了 SoftReference来实现软引用缓存。localcache通过软引用,来方便在内存不足时能自动回收缓存对象,来避免 OOM 的发生。

弱引用比软引用更弱。被弱引用指向的对象只能存活到下一次垃圾回收之前。弱引用是使用WeakReference来创 建对象的方式,WeakReferenc。如果一个对象只有弱引用指向,即使系统内存充足,垃圾回收器也会立即回收它。

虚引用是最弱的一种引用。虚引用的存在不会影响对象的生命周期,垃圾回收器在回收对象时不会考虑虚引用。虚 引用是使用PhantomReference来创建对象的方式。虚引用主要用于跟踪对象被回收的状态,并在对象被回收后进行一些后续处理(如清理本地资源等)。虚引用通常与 ReferenceQueue结合使用,管理清理逗辑。它在很多需要资源清理的场景中使用,比如文件句柄、数据库连接等。

什么是G1垃圾回收器?为啥G1是jdk9默认垃圾回收器

G1,Garbage First,是CMS的改进版,解决了CMS内存碎片、更多的内存空间等问题。

  1. 并发回收:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop The World的停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发 的方式让Java程序继续执行。
  2. 分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它 能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效 果。也就是说G1可以自己管理新生代和老年代了。
  3. 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于标记-整理算法实现收集,从局部 (两个Region)上来看是基于标记-复制算法实现的,但无论如何,这两神算法都意味着G1运作期间不会产生 内存空间碎片。 4.可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  4. 支持热插拔:G1可以在运行时动态调整堆的大小,以适应不同的内存需求。

与其它收集器相比,G1变化较大的是它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新 生代和老年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。 同时,为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根 节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。

什么是ZGC垃圾回收器?

ZGC(Z Garbage Collector)是Java 11中引入的一种新的垃圾回收器,他是一个为了实现低延迟而设计的垃圾 收集器,具有以下几个特点:

  1. 低停顿:ZGC的目标是保证暂停时间非常短,ZGC 的目标是保持最大暂停时间在亚毫秒级,且这个暂停时间 不会随着堆、live-set 或 root-set 的大小而增加。

  2. 高吞吐量:ZGC 是一个并发垃圾收集器,意味着大部分垃圾收集工作都是在Java 线程继续执行的同时完成 的。这极大地减少了垃圾收集对应用程序响应时间的影响。

  3. 兼容性:ZGC与现有的Java应用程序完全兼容,并且无需更改代码即可使用。但是也有一定的限制,仅支持 Linux 64位系统,不支持32位平台。不支持使用压缩指针,采用内存分区管理。

  4. 简单性:ZGC设计简单,代码库较小,因此它更容易维护和扩展。

  5. 支持大堆:ZGC 能处理从 8MB 到 16TB 大小的堆,适用于大规模内存需求的应用程序

  6. 不分代回收:ZGC在垃圾回收时对全量内存进行标记,但是回收时仅针对分内存回收,优先回收垃圾比较多 的页面。(JDK 21中已经支持分代ZGC了,JDK 24中删除了非分代ZGC) 因此,ZGC是

  7. 一种新的、高效的、低停顿的垃圾回收器,适用于内存大小从几GB到数TB的应用程序。它的设计目 标是在保证高吞吐量的同时保证最短的暂停时间,并且易于使用和维护。

jdk8和jdk11的GC有什么特点?

jdk8是新生代和老年代的垃圾回收,jdk11是整体的整堆回收。

jdk8默认垃圾回收器:parallel old+parallel sea。jdk9就是g1。 jdk21就是zgc。

G1和CMS有什么区别?

G1 是JDK 1.9中默认的垃圾收集器,他代替了Java 8 中的默认的Parallel Scavenge GC +Parallel Old GC,并且 也代替了CMS. G1 和 CMS相比,他们都是基于三色标记法实现的,替代了原有的传统的可达性分析(三色标记也是可达性分析 的一种,只不过特殊一点),可以大大的降低STW的时长。但是,他们之间还是有很大的不同的;

CMS主要用于老年代,会产生内存随便,堆内存要求不高,不支持可预测性

总结一下就是,G1会把Java的堆分为多个大小相等的Region(每个Region的大小为1M-32M),他在年轻代回收 的时候采用标记-复制算法,而在老年代回收的时候,采用的是标记-整理算法,这两种算法都可以避免内存碎片 的产生。

G1在回收的过程中,标记和清理的过程是并行的,可以充分利用多个CPU来缩短STW的时长,在复制的过程中是 并发的,可以让复制线程和用户线程并发执行,不需要STW。并且G1还可以在运行时动态的做区域内存大小的调 整。

java中类的生命周期?

加载/链接/初始化。链接过程又包括:验证,准备和解析。

类的使用,类的卸载。该类的所有实例都被gc回收,该类的classLoader已经被回收。比如。自定义加载类一些场景的类会被回收掉,比如tomcayt,spi。jsp等临时类,是存活不久的。

java中的类加载过程怎么样子的?

Java中类的加载阶段分为加载(Loading)、链接(Linking)和初始化 (Initialization)。其中连接过程又包含了验 证、准备和解析。

加载阶段的目的是将类的.class文件加载到JM中。在这个阶段,JVM会根据类的全限定名来获取定义该类的二进 制字节流,并将这个字节流所代表的静恋存储结构转换为方法区的运行时数据结构。 加载过程会创建一个java.lang.Class类的实例来表示这个类。这个Class对象作为程序中每个类的数据访问入口。

在链接阶段,Java类加载器对类进行验证、准备和解析操作。将类与类的关系《符号引用转为直接引用)确定 好,校验字节码

  1. 验证:校验类的正确性(文件格式,元数据,字节码,二进制兼容性),保证类的结构符合JVM规范。
  2. 准备:为类变量分配内存并设置类变量的默认初始值,这些变量使用的内存都在方法区中分配。(这里初始化的是类变量,即static字段,实例变量会在对象实例化时随对象一起分配在Java堆中。)
  3. 解析:把类的符号引用转为直接引用(类或接口、字服、类方法、装口方法、方法类型、方法句柄和访问控制修饰符7类符号引用)

java中的类什么时候会被加载?

  1. 当创建类的实例时,如果该类还没有被加载,则会触发类的加载。例如,通过关键字new创建一个类的对象 时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。
  2. 当使用类的静态变量或静态方法时,如果该类还没有被加载,则会触发类的加载。例如,当调用某个类的静态方法时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。
  3. 当使用反射机制访问类时,如果该类还没有被加载,则会触发类的加载。例如,当使用Class.forName()方法加载菜个类时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。
  4. 当JVM启动时,会自动加载一些基础类,例如java.lang.Object类和java.lang.Class类等。 总之,Java中的类加载其实是延迟加载的,除了一些基础的类以外,其他的类都是在需要使用类时才会进行加数。同时,Java还支持动态加载类,即在运行时通过程序来加载类,这为Java程序带来了更大的灵活性。

总之,Java中的类加载其实是延迟加载的,除了一些基础的类以外,其他的类都是在需要使用类时才会进行加 载。同时,Java还支持动态加親类,即在运行时通过程序来加载类,这为Java程序带来了更大的灵活性。

什么是双亲委派?如何破坏?

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,先检查是否已经加载过这个类,如果有则直 接返回,然后它也不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加戟器中,只有当父加载器反馈自己无法完成这个 加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加親。

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在 java.lang.ClassLoader的loadClass()方法之中,代码简单,還辑清晰易懂:先检查类是否已经被加载过,若没有 加載则调用父加载器的loadClass0)方法,若父加载器为空则默认使用启动类加载器作为父加戟器。如果父类加载 失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。 双亲委派模型主要是由 ClassLoader#loadClass 实现的,我们只需要自定义类加载器,并且重写其中的 loadClass方法,即可破坏双亲委派模型。

如何判断JVM中类和其他类是不是同一个类?

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用劫远远不限于类加载阶段。对于任意一个 类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一 个独立的类名称空间。

简单点说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

JVM如何保证给对象分配内存过程的线程安全?

  1. 如果JIT的逃逸分析后该对象没有逃逸,那么可能优化到栈上分配。
  2. 否则对象主要分配到新生代上,如果启动了TLAB,则分配到TLAB中。
  3. 如果被判断为大对象,則直接分配到直接进入老年代,譬如很长的字符串和数组,避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。可以设置 -XX:PretenureSizeThreshold,令大于该尺寸的 对象直接进入老年代。

当给对象分配内存的时候,有可能在栈上分配,这自然不存在线程安全问题。除此之外,如果在堆上分配,则可能会启动TLAB机制,使得堆内存给线程单独划分空间,避免了线程安全的问题。

同时,当不启动TLAB机制的时候,如果一个空间被多个线程同时分配对線,JVM会采用CAS+失败重试的方式来 避免线程问題。(具体的CAS机制和其利弊可以移步到JAVA并发专栏)

简而言之,是采用乐观锁的方式,只有假定该堆没有被其他线程操作的时候,当前线程才会在堆上分配对象,如果 被其他线程操作,就获取当前堆中的最新标识,然后重试

虚拟机中的堆一定是线程共享的吗?

并不一定哦!为了保证对象的内存分配过程中的线程安全性,HotSpot虚拟机提供了一种叫做TLAB(Thread Local Allocation Buffer)的技术。在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,当需要分配内存时,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

所以,“堆是线程共享的内存区域”这句话并不完全正确,因为TLAB是堆内存的一部分,他在谈取上确实是线程共 享的,但是在内存分分配上,是线程独享的。

TLAB的空间其实并不大,所以大对象还是可能需要在堆内存中直接分配。那么,对象的内存分配步骤就是先尝试TLAB分配,空间不足之后,再判断是否应该直接进入老年代,然后再确定是再eden分配还是在老年代分配。

JVM调优工具

JVM工具主要用来监控JVM的,这类工具主要分为两大类,第一类是JVM自带的,比如jstat、jmap等,还有依赖 是第三方的,如VisualVM等。

jps: JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常透合在linux/unix平台上简单察看当 前java进程的一些简单情况。

jstack:Java虚拟机自带的命令行工具,主要用于生成线程的堆栈信息,用于诊断死锁及线程阻塞等问题。

jmap:Java虚拟机自带的命令行工具,可以生成JVM中堆内存的Dump文件,用于分析堆内存的使用情况。排查 内存泄漏等问题。

jhat:使用jmap可以生成Java堆的Dump文件,生成dump文件之后就可以用jhat命令,将dump文件转成html的 形式,然后通过http访问可以查看堆情况。

jhat有什么用?如何用他分析堆dump?

Ihat(Java Heap Analysis Tool,是一个用来分析java的堆情况的命令。使用jmap生成的Java堆的Dump文件可以用 jhat命令将其转成html的形式,然后通过http访问可以童看堆情况。 jhat命令解析会Java堆dump并启动一个web服务器,然后就可以在浏觉器中宣看堆的dump文件了。

有哪些常用的JVM启动参数?

  1. 堆设置:-Xms:设置堆的初始大小。-Xmx:设置堆的最大大小。
  2. 找设置:-XSS:设置每个线程的栈大小。
  3. 垃圾回收器设置:-XX:+UseG1GC:使用G1 垃圾回收器。-XX:+UseParalleIGC:使用并行垃圾回收器。
  4. 性能调优: • -XX:PermSize 和 -XX:MaxPermSize:在 Java 8 之前设置永久代的初始大小和最大大小。 • -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize:在 Java 8 及以上版本设置 Metaspace 的初始大小 和最大大小。 。 -XX:+PrintGCDetails:打印垃圾回收的详细信息。
  5. 调试和分析:-verbose:gc: 输出垃圾回收的详细信思。-XX:+HeapDumpOnOutOfMemoryError:在内存溢出时生成堆转储。

一个对象的结构是怎么样?

HotSpot JVM设计了一个OOP-Klass Model. OOP (Ordinary Object Pointer)指的是普通对象指针,而Klass 用来描述对象实例的具体类型。

每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表 示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这 个对象中包含了对象头、实例数据以及对齐填充。

  • 对象头(Object Header):对象头是每个Java对象的固定部分,它包含了用于管理对象的元数据信息。对象 头的结构在HotSpot中是根据对象的类型(即是否是数组对象、是否启用偏向锁等)而变化的,但一般情况 下,对象头包含以下信息: Mark Word(标记字):用于存储对象的标记信忠,包括对象的锁状态、GC标记等。 Class Metadata Address(类元数据地址):指向对象所属类的元数据信息,包括类的类型、方法、字段等
  • 实例数据(Instance Data):实例败据是对象的成员变量(字段)的实际存储区城,它包含了对象的各个字段的值。实例数据的大小取决于对象所包含的字殷数量和字段类型。
  • 对齐填充(Padding):对齐填充是为了使得对象的起始地址符合特定的对齐要求,以提高访问效率。由于虚拟机要求对象的起始地址必须是8字节的倍数(在某些平台上要求更大),因此可能需要在对象的实例数据末尾添加额外的字节来对齐。

JVM是如何创建对象的?

  1. 首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和始化过。如果没有,那必须先执行相应的类加载过程

  2. 分配内存。JVM会在堆中为对象分配内存空间(元JT优化情况下)。在HotSpot中,对象的内存分配有两种方式,分别是指针碰撞和空闲列表法。

指针碰撞:当堆中的内存是连续的,JVM使用一个指针来标记当前可用的内存位置,然后将指针向前移 动分配对象所需的内存大小。

空闲列表;当堆中的内存是离散的,JVM会维护一个空闲列表,记录可用的内存块。在分配对象时, JVM会這历空闲列表,找到足够大小的内存块进行分配。(分配内存解决并发有两种手段,一个是CAS+失败重试,一个是Thread Local Allocation Buffer (TLAB)

  1. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步确保了对象的字段在创建时都有默认值。如int被初始化为0,引用类型被初始化为null

  2. 设置对象头。该实例所对应的类、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄,轻 量级锁等等信息

  3. 调用该类的构造方法,初始化对象。如按照程序员意愿进行赋值

  4. 返回对象引用,当对象完成创建之后,返回一个该对象的引用,后续Java程序就可以使用这个引用来操作对 象了。

字符串常量池是如何实现的?

字符串常量池(String Constant Pool)是Java中一块特殊的内存区域,用于存储字符串常量。

当程序中出现字符串常量时,Java编译器会将其放入字符串常量池中。字符串常量是不可变的,因此可以共享。 如果字符串常量池中己存在相同内容的字符串,编译器会直接引用已存在的字符串常量,而不会创建新的对象。 在HotSpot虚拟机中:

在JDK 1.6及之前的版本,字符串常量池通常被实现为方法区的一部分,即永久代(Permanent Generation),用 于存储类信息、常量池、静态变量、即时编译器编译后的代码等数据。

JDK 1.7开始,字符串常量池的实现方式发生了重大改变。字符串常量池不再位于永久代,而是直接存放在堆 (Heap)中,与其他对象共享堆内存。 之所以要挪到堆内存中,主要原因是因为永久代的 GC 回收效率太低,只有在FuIIGC的时候才会被执行回收。但 是Java中往往会有很多字符串也是朝生夕死的,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

使用双引号括起来的字符串变量,会被认为是常量。会在编译后进入class文件的常量池,运行阶段,进入字符串常量池。

Intern()提供方法,用于将字符串对象手动添加到字符串常量池

什么是方法区?是如何实现的?

方法区是Java虚拟机规范定义的一块用于存储类信息、常量、静态变量、编译器编译后的代码等数据的内存区 城。 方法区是线程共享的,每个虚拟机实例只有一个方法区。实现方法区的方式在不同的JDK厂商以及不同版本中可能 有所不同的。所谓规范是规范,实现是实现,两码事儿。

由于永久代有固定的大小,且不容易调整,因此在一些场景下容易导致内存溢出。例如,如果应用程序中使用大量 的动态生成类或者频繁地加载卸载类,就可能导致永久代溢出。 所以,从JDK 1.8开始,HotSpot虚拟机对方法区的实现进行了重大改变。永久代被移除,职而代之的是元空间 (Metaspace)。元空间是使用本地内存(Native Memory)来存储类的元数据信息的,它不再位于堆内存中。

JVM 中一次完整的 GC 流程是怎样的?

一般来说,GC的触发是在对象分配过程中,当一个对象在创建时,他会根据他的大小决定是进入年轻代或者老年 代。如果他的大小超过 -XX:PretenureSizeThreshold 就会被认为是大对象,直接进入老年代,否则就会在 年轻代进行创建。(PretenureSizeThreshold默认是0,也就是说,默认情况下对象不会提前进入老年代,而是直 接在新生代分配。然后就GC次数和基于动恋年龄判断来进入老年代。〕

在年轻代创建对象,会发生在Eden区,但是这个时候有可能会因为Eden区内存不够,这时候就会尝试触发一次 YoungGC。(会在YoungGC前做一次空间分配担保,如果失败可能直接触发FuIlGC)

年轻代采用的是标记复制算法,主要分为,标记、复制、清除三个步骤,会从GC Root开始进行存活对象的标 记,然后把Eden区和Survivor区复制到另外一个Survivor区。然后再把Eden和From Survivor区的对象清理掉。

这个过程,可能会发生两件事情,第一个就是Survivor有可能存不下这些存活的对象,这时候就会进行空间分配担 保。如果担保成功了,那么就没什么事儿,正常进行YoungGC就行了。但是如果担保失败了,说明老年代可能也 不够了,这时候就会触发一次FuIIGC了。

JVM为什么要把堆和栈区分出来?

堆和栈是JVM中的两个区域,想要知道为什么要搞两个区域,其实只需要描清楚他们的特点和用途之间区别是什 么就行了。

堆是存储对象的区城,堆的大小可以根据需要随时调整,堆的管理有垃圾回收器进行,堆内存是多个线程之间共享 的。栈是每个线程独享的一块区城,用于方法调用、局部变量等的存储。

把这两者区分开的好处有以下几个:

1.首先因为他们的存储内容不同,可以分开管理。堆内存可以用垃圾回收器管理,栈内存可以靠编译器和虚拟机执行完成。2.其次,可以做到不互相影响。独立开两个不同的区城,可以做到不互相影响。堆内存溢出不会影响到栈。栈溢出也不会影响到堆。3.还有就是可以做到数据隔离,因为有了栈,就可以把一些线程独享的局部变量等内容放到栈上,可以做到更好的隔离。而共享的一些数据就可以放到堆上做统一管理。4.提升各自性能。栈上分配的效率很高,可以适合分配局部变量等,可以非常的高效。而堆上的内存分配及回收都会相对复杂。这样区分开可以做各自的优化,提升整体效率。

运行时常量池和字符串常量池的关系是什么?

运行时常量池,是runtime constant pool,是Java虚拟机规范中定义的一块逻辑区域,它是方法区的一部分,规 范中说明了,它是用于存储常量、符号引用和一些编译期己知的常量数据。

因为Java虚拟机规范并没有规定要如何实现方法区,所以在不同的HotSpot的JDK版本中,方法区所处的位置是不 同的,所以运行时常量池所处的位置也是不一样的。

HotSpot为了复用字符串对象,定义了一个字符串常量池,它是作为字符串对象的缓存池,用于存储所有字面量形 式创建的字符串。

很多人认为字符串常量池和运行时常量池没啥关系,因为他们所处的位置不一样,尤其是在JDK 1.7之后,字符串 常量池在堆上,而运行时常量池随着方法区而处于永久代或者元空间。

但是,根据虚拟机规范,字符串常量,需要放在运行时常量池中。所以,我认为字符串池就是运行时常量池的一个 逻辑子区域。即字符串池是运行时常量池的分池!

什么是堆外内存?如何使用堆外内存?

堆外内存就是JVM之外的机器内存,

尽管堆外内存不受Java堆大小的限制,但它仍然受到系统可用内存的限制。如果操作系统没有足够的可用内存供 应用程序使用,就有可能导致堆外内存分配失败,从而拋出OutOfMemoryError。 想要使用堆外内存,有两种方式,分别是借助Unsate类以及NIO。

NIO中引入了ByteBuffer类,也可以用于处理堆外内存: 使用ByteBuffer类的allocateDirect()方法来创建一个DirectByteBuffer实例,它表示堆外内存的缓冲区。

但是,需要注意的是,堆外内存不受Java垃圾回收机制的管理。在不再需要堆外内存时,务必手动释放内存资 源,香则可能会造成内存泄漏和应用程序异常。因此,堆外内存的使用一般在特定场景和对内存管理有丰富经验的

内存泄漏/内存溢出的区别是什么?

内存泄漏指的是程序中分配的内存在不再需要时没有被正确释放或回收的情况。这会导致程序持续占用内存,随着 时间的推移,可用内存逐渐减少,最终可能导致程序性能下降或崩溃。内存泄漏通常发生在程序中的对象或数据结构被创建后,但没有适时地释放对它们的引用,从而阻止垃圾回收器将它们清理出内存。

常见的内存泄漏情况包括未关闭的文件或数据库连接、未释放的资源对象(如打开的文件句柄或网络连接)、长时 间被引用的集合类(List、Map)等。内存溢出指的是程序试图分配超过其可用内存的内存空间的情况。这通常会直接导致Java程序崩溃。

常见的内存溢出情况包括栈溢出和堆溢出。我们常说的内存溢出如果没有特别说明都是指堆溢出,即OutOfMemory

在Java中,当程序动态分配内存(例如使用new操作符在堆中创建对象)时,没有足够的可用内存时,就会发生 OOM,即OutOfMemoryError.

一般来说,内存泄漏是会导致内存溢出的,因为内存泄漏会导致部分内存一直无法被回收,久而久之就会没有内存 可以分配,就会导致内存溢出。

破坏双亲委派后,能重写String类吗?

但是我们虽然可以通过破坏双亲委派屏蔽Bootstrap ClassLoader,但无法重写 java.包下的类,如java.lang.String。

我们知道,要破坏双亲委派模型是需要 extends ClassLoader 并重写其中的 loadClass()和 findClass()方法。

之所以无法替换 java.包的类,主要原因是即使我们破坏双亲委派模型,依然需要调用父类中( java.lang ClassLoader.java的 defineClass()方法来把字节流转换为一个JVM识别的class。而 defineClass方法中通过 prebefineClass()方法限制了类全限定名不能以java.开头。

什么是Class常量池,和运行时常量池关系是什么?

Class常量池可以理解为是Class文件中的资源仓库。Class文件中除了包含类的版本、字段、方法、接口等描述信 总外,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。

Class是用来保存常量的一个媒介场所,并且是一个中间场所。Class文件中的常量池部分的内容,会在运行期加 载到常量池中去。

Java发生了OOM一定会导致JVM 退出吗?

JVM是一个操作系统的进程,而在Linux和其他类Unix操作系统中,当一个进程在执行非法内存访问 时,如访问未分配给它的内存或者访问超出其允许范围的内存时,操作系统会向该程序发送SIGSEGV信号(“段错 误”(Segmentation Fault),若进程没有注册信号处理函败会直接退出,并产生 Segment Fault 错误提示。 而我们熟知的OutOfMemoryEror,StackOverllowEror就是 Segment Fault 的具体情况,不过,JVM被设计 成能够容忍和隔离单个线程出现问题,当一个线程崩溃时,JVM会尝试将问题限定在该线程内,而不会影响其他 线程或整个应用程序。

主要是因为,OutOfMemoryError, StackOverflowError等这些我们看到的ERROR,已经是JVM在注册了SIGSEGV信号处理函数之后,经过自己的处理之后抛给我们的错误了

而OutOfMemoryError, StackOverflowError等这些错误抛给我们之后,其实都是可以被catch的,如果被catch 掉之后,程序还是可以正常执行,而不会崩溃退出的。

JDK1.8和1.9中类加载器有哪些不同

从Java开发人员的角度来看,类加载器还可以划分得更细致一些,不过,这里需要区分JDK的版本,在JDK 1.8及 之前的版本,和之后的版本是不太一样的。主要是因为JDK 1.9中提供了Jigsaw实现模块化,导致类加载器发生了 一些变化。 在JDK1.9中,原来的扩展类加载器被重命名为平台类加载器一PlatformClassLoader。

PlatformClassLoader 负责加载JDK平台本身的类库,这些类库位于JDK的 lib 目录下,但不包括核心的 java.* 类库(这些由启动类加载器加载)。它主要加载的是那些属于Java平台模块系统的非核心模块。这些模块在 module-info.java 文件中被明确声明。

什么是逃逸分析?

对象基于逃逸分析可以有三种状态:全局逃逸(GlobalEscape)、参数逃逸(ArgEscape)和无逃逸

  1. 全局逃逸(GlobalEscape):对象超出了方法或线程的范围,比如被存储在静态字段或作为方法的返回信息。由于对象可能被多个线程访问,全局逃逸的对象一般不适合进行栈上分配或其他内存优化。但JIT可能会进行其他类型的优化,如方法内联或循环优化。
  2. 参数逃逸(ArgEscape):对象被作为参数传递或被参数引用,但在方法调用期间不会全局遂逸。这种情况下,对象虽然作为参数传递,但不会被方法外部的代码使用。JIT可以对这些对象进行一些优化,例如锁消除
  3. 无逃逸(NoEscape):对象可以被标量替换,意味看它的内存分配可以从生成的代码中移除。这是最适合优化的情况。JIT可以来取多种优化措施,如在栈上分配内存,消除锁,甚至完全消除对象分配(标量替换)。这些优化可以显著提高性能,减少垃圾收集的压力

什么是AOT编译?和JIT有啥区别?

1、javac把java代码编译成字节码,然后由Java虚拟机解释执行。 2、JIT把java的字节码直接编译成机器码,然后由Java虚拟机直接运行。

JIT缺点

1.增加启动时间:由于JIT编译器在程序运行时编译代码,它可能导致应用程序的启动时间较长。 2.可能会影响应用性能:JIT编译是需要进行热点代码检测、代码编译等动作的,这些都是要占用运行期的资 源,所以,JIT编译过程中也可能会影响应用性能。

AOT编译,翻译一下就是提前编译,它不像JIT一样在运行期才生成机器码,而是在编译期间就将字节码转换为机器码,这就直接省去了运行时对JVM的依赖。这是一种典型的静态编译技术。

一个Java进程占用的内存都哪些部分?

堆-年轻代,老年代 /栈:虚拟机栈,本地方法栈

堆外内存包括了元空间、压缩类空间、代码缓冲区、直接缓冲区 4个部分。

元空间(Meta Space):从JDK 1.8开始,HotSpot虚拟机对方法区的实现进行了重大改变。永久代被移除, 取而代之的是元空间(Metaspace)。元空间是用来来存储类的元数据信息的。

压缩类空间(Compressed Class Space):压缩类空间是元空间的一部分,专门用于存储类的元数据,而且 在使用64位JVM时,通过使用较小的指针(通常是32位的指针)来引用类的元数据,从而减少了内存的使用 量。

代码缓冲区(Code Cache):主要用于存储编译器编译后的本地机器代码。当Java方法被JVM的即时编译器 (JIT编译器)编译成本地代码(Native Code)后,这些代码被存储在代码缓冲区中,以便后续直接执行,提 高程序运行效率。

直接缓冲区 (Direct Buffer):直接缓冲区 (Direct Buffer)是Java NIO中的一个概念,用于在Java程序和操 作系统之间高效地传递数据。与传统的Java I0相比,NIO引入了通道 (Channel)和缓冲区 (Buffer)的概 念,使得数据的读写更加高效。直接缓冲区就是这些缓冲区中的一种,其特点是它在物理内存中分配存储空 间,从而减少了数据在Java堆内存和操作系统之间来回复制的需要,提高了数据处理的效率。

非JVM内存

本地运行库指的是操作系统中用本地编程语言(如C或C++)编写的库,这些库直接运行在操作系统上,而不是在Java虚拟机(JVM)内部执行。

JNI (Java Native Interface)是一个编程框架,允许Java代码与本地代码(如C和C++代码)进行交互。

说一说JVM的并发回收和并行回收

我们说的并行回收其实就是Parallol GC,具体到垃圾收集器的实现上,就是Parallel Scavenge, Parallel old, ParNew等收集器,而我们说的并发回收比较典型的就是 Concurenct Mark and Sweep GC,也就是我们常说 的CMS,当然G1也是并发回收器。

那么,其实我们所熟悉的并行回收器主要的关注目标是吞吐量(吞吐量=代码运行时间/代码运行时间+垃圾收集 时间),所以他会想尽办法最高效率的利用CPU时间来进行垃圾回收。所以他会在垃圾收集期间通常会暂停应用程 序的执行(STW),以快速完成垃圾收集。

而并发回收器主要关注的目标是STW的时长,它允许垃圾收集线程在应用程序线程运行的同时执行部分垃圾收集 工作,从而减少了STW的时间。并发回收期间,只有在特定的收集阶段会发生短暂的STW。

为什么初始标记和重新标记需要STW,而并发标记不需要?

这里主要是CMS/G1三个标记法

在初始标记阶段,针对根(GCRoot)直接引用的对象进行标记,这个过程也通常被叫做根扫描。为了防止在初始 标记过程中根对象被修改,这个过程是STW的,虽然G1可以通过果用写屏障技术来获知对象是否发生了修改,但是因为大多数的GCRoot他并不是对象,所以无法被获知的,所以,这个阶段是需要进行STW的。

并发标记阶段。会标记所有从直接可达对象间接可达的对象,这个过程是不会STW的,也就是说用户的线程和GC 的线程是并发执行的。这样可以最大限度的减少应用的停頓时间。

重新标记阶段,目的是修正并发标记阶段因应用程序继续运行而产生的任何变化(因为并发标记没有STW,所以 会有变化)。此时,需要重新检查和更新那些在并发标记阶段可能发生变化的对象标记信息。重新标记是清理前的 最后一次标记,需要确保这个过程的准确性,所以需要做STW来保证。

总之,三个阶段,为了提升性能肯定是能不STW就不STW,而最后一个阶段——重新标记因为是最终阶段,所以 需要STW来确保准确性。而第一个阶段—一初始标记,因为无法感知到GCRoot的变化,所以需要做STW来确保 这个阶段的准确性。

什么是STW?

STW,是Stop-The-World的缩写,Stop-The-World是指系统在执行特定操作时,必须暂停(停止〉所有的应用程序线程。

比如在Java中,当需要进行垃圾回收的时候,垃圾回收器需要停止应用程序的所有线程,以便可以安全地识别和回收不再使用的对象。这个过程我们就会称之为是Stop The World了。

STW事件会暂时停止应用程序的运行。对于需要高响应性或实时性能的应用程序來说,这可能导致性能问题,因为它会导致响应延迟。

并且在在STW期间,应用程序的响应时间(RT)和吞吐量(QPS)都会受到影响,这可能导致性能的不可预测 性,特别是在负载较高的情况下。

为了減少STW带来的影响,需要对垃圾收集器的配置进行调优,比如选择不同类型的垃圾收集器、调整堆大小或者垃圾收集器的其他参数。

什么情况会导致JVM退出?

程序正常运行完 当Java应用程序中的所有非守护线程(即用户线程)都完成执行且没有其他活动线程时,JVM会正常退出。这是 最常见的退出方式。

System.exit()被调用 当代码中的任何位置调用 System.exit(int status)方法时,JVM会立即开始终止过程。这个方法可以接受一个状 态码,通常用0表示正常退出,非0表示异常退出。

Runtime.getRuntime().halt()被调用 与 System.exit()不同,Runtime.getRuntime().halt(int status)方法用于强制终止当前运行的Java虚拟机,而不 会执行任何关闭钩子或者终止已注册的未捕获异常处理器。

遇到无法恢复的错误当JVM遇到一个无法恢复的系统错误,如操作系统信号或内部错误,它可能会立即退出。比如JVM自身的bug或者 本地方法库存在一些问题等。

项目中如何选择垃圾回收器?为啥选择这个?

  1. 串行垃圾回收器(Serial Garbage Collector)RD: Serial GC, Serial Old

单线程垃圾收集器,适用于单核机器。暂停时间较长,不适用于多核环境和大内存应用。

  1. 并行垃圾回收器 (Parallel Garbage Collector) 如:Parallel Scavenge, Parallel Old, ParNew 多线程垃圾收集器,适用于多核机器。适合批处理、后台作业等暂停时间不敏感的应用。暂停时间较长,不适合需要低延迟的应用。

  2. 并发标记扫描垃圾回收器 (CMS Garbage Collector) 低延迟垃圾收集器,适合霜要响应时间较短、高响应性要求的应用。容易产生碎片,可能霧要Full GC进行碎片整理,Full GC时会有较长暂停。

  3. G1垃圾回收器 (G1 Garbage Collector, JDK 7中推出,JDK 9中设置为默认) 设计用于替代CMS,适合大内存、多核环境。软低的暂停时间,能够预测性地控制暂停时间,适合大数 据量应用。

  4. ZGC垃圾回收器(The Z Garbage Collector, JDK 11 推出)、Shenandoah GC (JDK 12 推出)

超低延迟垃圾收集器,适用于超大堆内存。暂停时间通常在10ms以下,适合对响应时间有极高要求的应 用。目前只支持较新的JDK版本,可能存在一些不成熟的特性。

元空间满了(或溢出),可能是什么原因?

1、类加载的太多了(最常见!) 类加载并不只是说你项目中的你写的Class太多了,这些往往都不是类加载太多的主要原因,而是需要考虑哪些须 紧动态生成类,比如:动态代理、CGLIB、JSP编译、Groovy脚本等等,他们都会在运行期动态生成类。

2、类加载器泄漏 失加载泄露,估计很多人不知道啥意思,其实就是同一个类名,被多个类加载器加载,因为会被认为是不同类,所 以每个类加载器持有它加裳的类,只要类加载器没有被 GC,类元信息就不能释放。就会持续占用你空间、。 比较常见的就是在热部署的情况下,比如Tomcat/IDEA热重启时类加载器没群放。 3、元空间太小 一般来说元空间用的都是堆外内存,默认都是可以自动扩容的,容量也都很大,但是如果有的时候你用 MaxMetaspaceSize指定了大小,那么也会容易被干满。

为什么JDK 1.8要废弃永久代,改用元空间

永久代是堆上的一部分,大小收堆大小的限制,容易发生0OM。元空间在本地内存上,不受堆大小的限制,不容 易发生OOM。 因为方法区作为元空间和永久代的主要区域,他里面存储了大量的被加载的类,而很多时候,项目中用了很多动态 生成类的框架之后,比如Spring、等等,就会占用很多空间,如果在堆上,就会导致频繁的GC,以及OOM的问 题。

ZGC和CMS和G1的区别对比?

从jdk版本,设计的目标,

stw: 部分stw/部分stw/所有阶段都是并发执行的

分代情况,年轻代+老年代 /划分为等大小的region,逻辑分代。/划分为等小大的region

回收位置: 老年代/整堆/整堆

gc算法:标记-清除/区域化的标记-整理算法/并发标记-整理+可拓展区域分配

碎片产生:存在内存碎片/可预防内存碎片产生

可预测性:无法预测/可预测

堆内存基本要求

spring

什么是IOC

ioc就是控制反转。传统程序,当我们需要对象时,是由代码直接new出对象。哪里new出是由我们控制。但是ioc不一样,由容器控制对象的创建。当我们需要对象,由容器注入进来。应用程序不在控制对象的创建。而是被动的接受由spring注入的对象

什么是AOP

aop就是面向切面编程。主要功能,把通用的业务逻辑抽象出来,让开发专注于业务的编写。比如一个下单功能,需要权限校验/事务管理/下单/日志打印上传。这里面业务逻辑只有 下单,剩下的三个功能都是比较通用的,可以通过切面完成。

  • Aspect切面。横切关注点的模块化抽象。它包含了AdvicePointcut。例如,一个“日志切面”或“事务切面”。
  • Joinpoint连接点。程序执行过程中一个明确的点,如方法调用、异常抛出、字段修改等。在Spring AOP中,特指方法的执行
  • Advice通知/增强。切面在特定连接点上执行的动作。有不同类型:
    • @Before: 前置通知,在方法执行之前执行。
    • @AfterReturning: 返回后通知,在方法正常返回后执行。
    • @AfterThrowing: 异常通知,在方法抛出异常后执行。
    • @After: 后置通知,在方法执行完成后执行(无论正常返回还是异常),类似于finally
    • @Around环绕通知,最强大的通知类型,可以包裹整个方法执行过程,可以控制方法是否执行、修改参数、修改返回值等。
  • Pointcut切点。一个表达式,用于匹配哪些连接点会被切入。AdvicePointcut是关联的,通知定义了“做什么”,切点定义了“在哪里做”。
  • Weaving织入。将切面应用到目标对象,从而创建代理对象的过程。Spring AOP在运行时通过动态代理完成织入。
  • Target Object目标对象。被一个或多个切面所通知的对象。
  • AOP ProxyAOP代理。由AOP框架创建的对象,用于实现切面契约。在Spring中,通常是JDK动态代理或CGLIB代理。

spring为啥不建议使用基于字段的依赖注入

可能造成npe问题。方法初始化顺序。静态代码块/实力初始化语句块/构造方法/@Autoried.

如果在构造方法中对 对象进行操作,发能发生控制住

原则问题。我们应该显示的告诉spring容器。有哪些对象需要被赋值和管理,而不是对springboot对象自己去赋值

spring的生命周期

主要分为创建/使用/销毁。

创建里面。主要有几个地方,实例化Bean。设置属性/前置操作/初始化操作/底定义初始化操作/后置操作

设置销毁的回调函数/

@PostConstruct、init-method和afterPropertiesSet执行顺序

首先会执行构造函数/注解/after/int-method

Springboot新特性

jdk17

Jakarta 9

aot-即时编译

spring nativate

Springboot事物传播机制

  • REQUIRED,如果不存在事务则开启一个事务,如果存在事务则加入之前的事务,总是只有一个事务在执行 •

  • REQUIRES_NEW,每次执行新开一个事务,如果当前存在事务,则把当前事务挂起

  • SUPPORTS,有事务则加入事务,没有事务则普通执行 •

  • NOT_SUPPORTED,有事务则暂停该事务,没有则普通执行

  • MANDATORY,强制有事务,没有事务则报异常 •

  • NEVER,有事务则报异常 •

  • NESTED,如果之前有事务,则创建嵌套事务,嵌套事务回滚不影响父事务,反之父事务影响嵌套事务。

Autowired和Resource的关系?

autowired是 spring提供的注解/ resouce是 java提供。

2个在使用的过程中,autowired是先byType。然后再byName。resource正好相反。

autowired支持构造器/setter/。resouce仅能再setter方法上/或者

BeanFactory和FactroyBean的关系?

beanfactory是springioc的一个接口,主要负责获取Bean实例/依赖注入对象/生命周期

factorybean主要是用来创建复杂的bean对象的。当spring中有bean实现了这个接口,spring不会直接创建这个对象,而是返回factorybean中getobject对象。

Spring中如何开启事务?

声明式事物/编程式事务。声明式事务主要是使用注解。编程式事务主要使用底层的api。比如TransactionTemplent

springboot 事务失效哪些常见

首先springboot声明式事务是由 代理实现,如果代理失效的话,它也会失效。

比如使用private方法,使用方法在同一个类中。static/ final

还有异常被捕获/开启异步线程/

Spring常见的设计模式

工厂模式:就是springioc。从工厂中获取Bean.

责任链:主要是springmvc请求。做一些权限校验

组合模式:springmvc中使用最多。参数解析器/返回值解析器

适配器模式:springmvc 各种adaptee

策略模式:springioc。比如拉账单。拼多多/抖音接口。

模版:TransactionTempleme。

观察者模式:springboot中的event。消息发送方/接受放接偶了。提供了一个总线

什么是三级缓存

一级缓存。已经创建好的Bean。二级缓存,就是创建一半的BeaN. 三级缓存就是 一个创建Bean的工厂。

a->b b->a。我先创建a实例。把创建A的工厂,放入三级缓存。寻找b实例。创建b实例。把创建B的工厂放入三级缓存。b尝试寻找a的实例。发现三级缓存中存在工厂,由工厂先创建一个引用对象。然后把a工厂从三级缓存中移除,放入二级缓存。b的实例创建完成。删除二三级缓存。放入一级缓存。然后a也能拿到b的实例。放入1级缓存,清除二三级缓存。

什么是MVC?

Model-view-controll。将视图和模型进行接口,由控制器进行视图和模型的对接。

springmvc的流程

首先前端调用接口。被dispatchsevlert接收到。处理器映射器 根据url找到对应的handler。handler找到对应的controller。执行代码。返回modelandview。交给视图解析器,视图解析器把处理好的数据交给dispatchservlet。

再返回给前端

springboot自动配置的

我们通常在主类使用@SpringBootApplication注解,而这个注解是一个组合注解,它内部就包含了@EnableAutoConfiguration。所以,这个注解是开启自动配置的总开关。

springboot如何实现main方法启动web项目

springboot启动时,会执行main方法,里面有内嵌tomat服务。tomcat服务器。然后根据包名配置,扫描不同的包路径。将bean初始化完成。这样子就可以通过访问http:localhost:8080来访问服务器。

自动配置的加载机制:AutoConfiguration.imports`文件Spring Boot会去扫描我们项目中所有jar包里存在的 /META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。 (补充说明:*在Spring Boot 2.7之前,这个信息是定义在spring.factories文件中的,但2.7版本标记为废弃,

这些自动配置类并不会全部生效,它们上面充满了大量的 @Conditional系列注解。这些注解就像是“开关”,只有满足特定条件时,配置类里的Bean才会被创建。

@ConditionalOnClass:当类路径下存在某个类时,配置才生效。(例如,有DataSource类才配置数据源)@ConditionalOnMissingBean:当容器中没有某个Bean时,配置才生效。这给了我们极大的灵活,意味着如果我们手动在配置中定义了自己的Bean,那么Spring Boot的默认自动配置就会失效,优先使用我们自定义的Bean。@ConditionalOnProperty:当指定的配置属性有特定值时生效。

为啥不建议使用@Async

因为async底层用simple。它的execute方法只有创建新线程。然后直接使用。没有线程复用

所以一般用法就是 使用自定义线程池。

event

是一种观察者模式。事件的发布和 事件的接受解偶。

主要分为3块,事件/事件发布者/事件监听器

代码接口/每个接口指责十分清晰/异步调用

Spring中@Service 、@Component、@Repository等注解区别是什么?

从功能角度没有差别。主要是业务。帮忙业务理解

component通用的组建/service业务处理/Repository 专注于数据库的开发/

如何实现缓存预热

使用Applicationreadtyevent/postsruct/initbean。

springboot中的bean是线程安全的吗?

无状态的单例的bean都是线程安全的。但是里面如果存在一些变量。还想进行修改。有可能是不安全的。

还可以设置@Scope注解。修改bean的作用域。变成有原型的,每次使用都创建一个新的也是线程安全的

springboot 中bean作用域

一共有6种类型。单例/原型/请求/会话/应用程序/websocket

Springboot如何自定义一个starter

首先需要使用@Configuration注解。然后还可以根据 @ConfigurationProperties/@ConditionalOnMissing等注解配合使用

然后创建配置配置类入口文件。

Mata-info下面创建imports结尾的配置文件

为什么springboot3中移除了spring.factories

主要是为了实现云原生/springboot使用vm直接编译成原生镜像。

java使用注解2种方式,一种是反射。另一种是apt。就像lombok.生产get/set一样。

结合 @AutoConfigure注解。借助apt 实现编译期间/检查处理注解。生成源代码。配置类

如何动态的控制生成bean

使用 @Conditional

使用@ConfigProperties

使用Profile

springboot中双亲委派

Bootstrapclassloader->systemclassloader->appclassloader.

jdk9中使用 platformclassloader代替了system。先让父亲加载,如果加载不到,则自己加载,委派为父亲。

好处就是 避免核心核心类被修改/重复加载/

sprinngboot 为了支持fatjar。jar包内部内嵌的jar包。它使用了一个 lanunaledUrlClassLoader加载器

当加载fat jar时,实现子优先 策略。尝试从自己jar包中寻找.boot-int/classes boot-info/lib下面寻找。如果找不到,再委派给父亲。主要是怕内嵌的代码会被外面旧版本类给覆盖掉。导致不可预见的问题

spring如何实现流式输出

  • 使用responseentity+streamingresponsebody
  • sseemiter
  • flux

Mysql

mysql中锁的分类

使用方式:乐观锁/悲观锁。

按照锁的粒度:全局锁/表锁(mdl锁(字典锁),auton-inc,意向锁)/行锁

锁级别:排他锁/共享锁。有共享锁,能继续申请共享锁,不能申请排他锁。

锁的对象:记录锁/间隙锁/临键锁

mysql中InnoDB和MyISAM有什么区别?

首先innodb支持事务/myisam不支持

索引不一样.innodb 聚簇索引/ 索引和数据 都是放在一起的

行锁级别不一样。myisam只支持表锁。innodb支持到行锁

innodb支持外健/不支持外健

mysql中char和varchar区别

char是定长的字符串0-255个字符串,varchar可以长度的字符串0-6553。适合存储身份证/国家编码 等信息

char无需多余空间存储来存储字符疮毒,不会造成页分裂,但是会有空格去除的风险

varchar需要多余的空间来存储字符长度。频繁的修改可能会造成页分裂。造成io性能损耗

MySQL 5.x和8.0有什么区别?

mysql8.0性能提升2倍。更好nosql。新增窗口函数/支持json/取消缓存查询,避免缓存查询带来一系列问题。命中条件苛刻,锁竞争严重/有更高级别安全性

mysql中为啥不推荐join

mysql在使用join 是使用嵌套循环完成。分为内循环/外循环/。算法时间复杂度都很高。最简单的算法,如果是表做关联。一个表为n。2个表就是n2。如果使用索引/则把符合条件的索引拿到外循环。如果使用内存算法,则提前把符合条件的数据拿到外循环。

一般做法就是 join变成java代码。或者冗余字段。如果数据是终态不大发生变化。则可以考虑宽表。

mysql语句执行流程

会使用连接器,检查该连接是否有权限/安全

mysql8以前会检查是否有缓存。如果缓存直接命中。则直接返回。

语法解析器解析sql。变成语法数。然后预处理,查看字段是否存在,语法是否合理。

然后进行查询优化中,看下是否可以优化索引。调用执行器进行执行

mysql支持几种行格式

一共有4种格式。第一种compact。分为2部分。第一部分,存储可变长度列表/头信息/null列表。第二部分是真实信息

第二种是reducement(冗余).

第三个是dynamic。对compact进行了增加。能够更好对可变长度列表进行拓展。对存储空间和查询时间进行了平衡

第四种是 compress。对数据进行压缩。当查询时,对数据进行解压缩

mysql中数据库事务机制

数据库的事务机制,主要就是acid.

a就是原子性,事务要么发生,要么不发生

c就是一致性,事物从一个从到另一个状态。

i就是隔离性,多个事务并发互相不影响

d就是持久。事务提交后,数据 必须被存储下来。

InnoDB的一次更新事务过程是怎么样的?

首先会检查bufferpool中是否存在数据。如果不存在,则从磁盘中获取到该数据。

然后新增undo log日志,主要是用来回滚和实现mvcc。将bufferpool中状态页变成已修改未提交状态。将bufferpool和undolog写到磁盘上,都是异步操作的

开始2阶段提交。新增redo log日志。状态prepare状态。接来下,新增binlog日志。然后提交。

如果有redo log。单子状态为prepare。没有bingo。则需要使用undolog日志进行回滚。

如果redo log.并且有bing log。机器重启后,则重新进行提交

mysql中事务的隔离级别

数据是支持多个事务并发进行。

隔离级别:事务并发过程中,不同事务时间数据可见性和互相影响程度

  • READ UNCOMMITTED未提交读,
  • READ UNCOMMITTED已提交读,
  • REPEATABLE READ可重复读,
  • SERIALIZABLE串行话

mysql中什么是脏读、幻读、不可重复读?

脏读:一个事务修改数据,但是并未提交,另一个事务能够读取到未提交到数据

不可重复读:一个事务第一个读取,然后另一个事务修改了数据,当第二次读取到时候,发现查询出来的结果不一样

幻读:在做范围查询的时候,另一个事务插入了数据,导致范围结果不一样

mysql中什么是readview-mvcc

readview是构建mvcc的基础,当我们进行一个select查询的时候,就会创建一个readview。包含当前数据的一个快照信息/正在执行但未提交的事务的集合/当前活跃的越大事务id/最小活跃事务id.

数据库中有些隐藏字段。包括trx_id。当前数据修改的最新事务id。roll_poniter是该行数据的上一个版本指针放在 undolog日志里面。

根据规则。通过undolog版本链 找到当前事务开始前最新的数据。

mvcc可以解决脏读和不可重复读。innodb使用mvcc+间隙锁方式解决幻读。把一个区间的范围都给锁住。这段区间内不允许插入数据。

select * 会使用到事务吗?

Select * 也是会使用事务,即使没有明确开启事务的语句,执行引擎也是会实现一个隐藏事务,只是这种事务不持有任何锁,执行会立刻提交,自动commit

mysql中binlog有几种格式

Statement/row/mx

statememt就是记录原生的sql。缺点就是主从同步的时候,会导致不一样,比如delete语句的时候,限制删除的条数,但是没有order by。这样子就会导致两边删除不一样。

row就是原生记录。每一行修改了什么都原封不动的进行修改。缺点就是日志的量比较大。当进行update语句时,使用的就是一样

mixed就是混合,执行器会根据sql情况来判断到底使用哪种格式

mysql中悲观锁/乐观锁

悲观锁。先加锁,再干活。乐观锁先干活后加锁。二者使用场景不一样。

悲观锁使用高并发写入场景。乐观锁使用读多写少。悲观锁一般是用select for update来实现的。

乐观锁一般是通过cas的方式,增加一个版本号来实现。更新的时候,使用id+版本号。来进行更新

MySQL的行级锁锁的到底是什么?

根据锁住的对象不同。我们可以分为 记录锁/间隙锁/next-keylock

记录锁,就是在唯一索引上进行加锁。

间隙锁。在索引之间的缝隙上加锁。不允许数据进行插入操作

next-key是二者的一个结合。记录锁+间隙锁。范围是前开后闭区间

使用原则:默认加的就是 next-key锁。如果是rr级别。查询的时候,访问到的对象才加锁

然后当等值查询,并且符合提交。退化成行锁。如果是等值查询,向右最后一个不满足条件,退化成间隙锁

mysql中共享锁/排他锁

共享锁就是读锁。当一个事务获取到读锁/则其他事务也可以增加读锁,但是不可以获取排他锁

排他锁就是写锁/当一个事务获取到写锁,则其他事务不能获取共享锁以及排他锁

mysql中意向锁

就是为了更好的管理行锁/表锁。

比如当事务1向表1设置了行锁。这一行记录不能被修改。事务2向表1设置了表锁。如果申请成功,则表示这个表数据都可以被其修改,这样子就发生了冲突。mysql为了避免这样子的问题,设置了意向锁。当事务来获取锁。会先判断是否有事务获取到了这个锁,看他是什么类型,是否和自己的锁冲突。如果不冲突则正常进行。如果冲突则等待。

mysql中加索引/会锁表吗?

Mysql 5.6之前是会,mysql5.6之后使用 online ddl技术。包括copy,instance/inpact。

首先对创建临时表,对表增加 共享mdl锁,只允许读取数据,不允许写。

然后同步数据/同步完成后,使用排他mdl锁,对表进行rename。构建索引/结束

Online ddl.技术,并不是正对所有ddl操作都支持,只是尽可能减少锁表时间。然后刚开始/结束的时候,也是需要短暂的锁表。ddl不会立刻执行,需要等所有事务提交提交开始执行,online ddl操作需要耗费大量的io。

所以最好非业务高峰期执行

mysql中索引类型

innodb中最常见的索引就是 B+树索引。hash索引

b+树索引分为聚簇索引和 非聚簇索引。聚簇索引所有的数据在叶子结点上。非业务节点都为索引信息。

找到业务叶子结点,就能找到该记录的所有信息

非聚簇索引,叶子节点存储的是主键信息,如果想要数据,则会触发回表操作

InnoDB为什么使用B+树实现索引?

首先B+树是一个平衡树,数据都在叶子节点上。这样子每次查询次数固定。io稳定

不像b树。每次查询次数不固定,io不稳定

b+数的层数不高。能够大幅度减少io操作,叶子结点又有双向链表。能够支持范围查询。

节点大小固定,能够使用磁盘预读的特性,提高范围查询的性能

hash索引 是通过hash值来获取桶的一个位置,插入/删除比较方便。不像B+树,比较构建和维护索引,需要页分裂和页合并,比较浪费性能。hash适合等值查询,不适合范围查询,因为它的数据是 无序的

mysql支持唯一性索引是如实现的

mysql的事务机制+锁 保证的。当一个事务进行新增时,会检查是否该索引值是否存在,如果存在,则会阻止插入

mysql中order by 是怎么实现的

order by 排序,主要看优化器的选择,如果能走索引排序,是最快。如果不能走索引。就会触发filesort操作。mysql会分配buffer_size大小的空间,如果排序的数据大小小于 size空间,则会基于内存进行排序,如果超过,则会使用磁盘辅助进行排序,使用归并算法。如果数据的当行大小超过一个阈值,排序的对象就是order by列。排序后就会触发回表操作。然后返回结果。

mysql中三大日志

Bingo/undolog/redolo

undolog就是回滚日志。主要记录数据历史版本。就像使用ctrl+z一样。主要用来回退到上一个版本。用途保持事务的原子性/一致性。mvcc。undolog主要是重做日志,当发生宕机的时候,用来回复数据的。保持事务的持久性。binlog主要是做数据同步的。主从同步。

通过一个update执行流程。就可以说三者之间的关系。当我执行一个UPDATE语句。当我从bufferpool中查询到数据,如果没有则从磁盘上获取。更新undolog。日志先行。在更新bufferpool。bufferpool中记录的是 已修改但是未提交。然后开始2阶段提交。记录redolog日志,更新为prepare状态,在更新binlog日志。然后提交。其中任何阶段失败,都有日志进行回滚/重做。比如binlog/undolog都没有。

那说明事务根本没有成功,直接根据undolog进行回滚。如果redolog成功,但是binlog没有成功,也是一样,进行回滚。如果redolog成功,binlog也成功,但是事务没有提交,则根据redolog提交事务。完成事务。

undolog是循环写。日志大小是固定。不是全量日志文件。所以不能做备份/数据同步。binlog日志是全量日志

mysql中使用了索引还很慢

选错了索引。优化器执行错了计划/这个时候就需要强制绑定计划

索引分布不均匀。根据日期进行索引。某一天的量占一个表的一半。这个时候就会走全表索引扫描,很慢

sql语句不正确。返回了太多了字段。

mysql中sql执行计划分析的时候,要关注哪些信息

首先关注selecttype。比如simple.union。越简单,查询效率越高

然后type字段,避免all, index。剩下的基本上都可以比如system.conf.eq_ref,ref,range

关注key字段,查询计划使实际使用到索引。有时候通过索引优化,可能执行的时候并不是想象中的索引

keylen字段,当有多个索引值的len长度越小,执行效率越高。

RoWS,生产最好小于10w。

extra里面有一些信息fiesort,覆盖索引等信息

如果是tidb的话,也是一样的 。避免fullscan,走indexscan,行数小于10w .内存大小加起来小于1m b

mysql中如何优化数据

表结构设计,设计冗余字段,避免多表join。或者把join操作变成java代码。或者直接做成宽表/ 定期归档历史。避免表太大,表小什么都好说。

查询/索引优化:我们根据explain分析sql执行的计划。

使用缓存。讲热点数据/不易变化 直接返回

冷热分离/主从同步。讲读写进行分离

数据量再大就是 分库分表mycat/使用分布式数据tidb.clickhouse,hbase。

把数据放在一个服务区内部/公司的内部网络基本上纳秒级别。上高性能上磁盘

MySQL只操作同一条记录,也会发生死锁吗?

会,因为我们的记录并不是锁住记录,而是索引

当查询条件是普通索引,会普通索引进行加锁,然后再回表对主表索引进行加锁

如果另一条线程对主表进行加锁,再尝试对.

mysql中如何解决死锁

死锁产生4个提交:

互斥:资源只能被一个占用,不可被共享。

占用且等待:资源一单被占用,不可被动释放,除非自己主动释放。

不可剥夺:被占用的资源不可被剥夺

出现环结构,循环等待。

兜底策略,数据库会设置锁等待时长,如果超过了这个时间,会选择回滚一个或者多个事务。

代码里面:减少食物时长,加快代码执行速度。减少锁的持有时长。使用rc来代替rr。

顺序访问数据/

mysql中索引失效问题

is not null/not in /!=/like/or/隐式转换/函数计算/表太小/索引区分度不高/没有符合最左前缀匹配原则

mysql8 支持功能索引。对列进行计算就可以使用

mysql中如何进行sql调优

索引失效/多表join/查询字段太多/数据量太大/连接数不够多/表结构不合理/数据库参数不合理/事务太长,长时间占用连接/深度分页/回表次数太多

mysql中区分度不高的字段建索引一定没用吗?

没有,我的项目里面有个任务,我每次扫描都是获取初始化/失败的。这2类的任务都非常少。构建索引的话就会很快。

mysql中主从同步

服务器会有2个线程。io线程/sql线程,io主要跟主服务器的dump教程交互,告诉dump线程需要自己从哪个位置拉取数据。io拉取数据数据,会写到relay log中。sql从relaylog中 读取数据写道mysql。复制主要有几种类型。

第一种异步。主服务器只管写,不管从服务器是否接受到消息。当主服务器宕机。主服务器提交没有同步到从库中,会导致数据出现差异。全量:等所有从服务器都接受到消息,再事务提交。性能较差。

半同步:就是等一个从库从库接受到消息,则提交事务

mysql中数据页

b+数叶子结点的基本单位

数据页主要包含:文件头/页头/最小最大几率/用户嘻嘻/空闲空间/文件尾部

mysql中驱动表

如果使用了join.使用的就是 嵌套查询。嵌套查询有几个算法。如果2个表没有索引。一个表为n。复杂度n2。如果有索引 log^n N.如果内存算法。驱动表把一部分查询结果放到外循环,外循环提前坐下筛选。复杂度都很高

优化器更加倾向于。小表驱动大表。至于哪个是驱动。使用expain。第一行就是驱动表/第二行就是 被驱动表

mysql中事务比较长

锁竞争严重/长时间占用连接池/mvcc性能/难以回滚/主从延迟/

SQL中PK、UK、CK、FK、DF是什么意思?

pk主键约束,uk就是唯一键约束 ck检查约束,fk是外健约束。df是默认约束

mysql中什么是buffer_pool

数据都在磁盘,mysql为了提升性能。使用了缓存,当读取到数据,会数据库加载到buffer_pool。为了利用程序局部性的原理,每次一加载数据,都加载一页的数据。当访问某一条数据,大概率会访问接下来的数据。默认书128mb.

mysql中自增主键用完了

显示的自增主键id用完,会报错。隐式的会覆盖历史数据。

做法就是归档历史数据。

mysql中组提交

它会将多个并发没有冲突的sql.放在一起进行prepare。一起写入磁盘。加快速度。从而减少io操作

mysql中并行复制

为了降低日志太多导致的主从延迟。实现了并行复制

基于库并行复制,5.7基于组提交并行复制,8.0基于writeset并行复制。

基于组提交,sql肯定没有冲突,在从库上先执行后执行,肯定没有冲突

mysql深度分页

使用子查询+join的方式。子查询返回id。然后直接id直接查询数据。

使用id游标,记录上一次返回的id。

什么是数据库的主从延迟?如何解决?

从库复制主库数据 出现延迟,

网络问题/性能问题-日志太多或者线程数太少/大事务

提高网络配置,再同一个区域内部/并行复制。增加服务器性能/减少事务时长

mysql中为啥取消缓存查询

频繁失效,命中条件比较苛刻,只要有数据更改,匹配字符串有改动,缓存就会失效

内存开销:失效过快,新增/删除内存开销大

不一致性:缓存不一致性

查询分布不均匀,导致命中率较低

mysql中where条件的顺序影响使用索引吗?

因为有的优化器的存在,字段的先后并不影响。

Mysql什么是字典锁(mdl锁)

METALOCK DARA. 就是元数据锁。表锁的一种。当进行ddl的操作的时候,就会使用它,举个例子,当修改缩影的时候,使用o nlineddl技术,首先会创建一个临时表/竞争共享m d l锁,等数据同步后,就会获取 排他mdl锁。然后进行rename操作。创建索引。

mysql中为什么不使用分库分表?

性能问题:每次插入数据/删除修改数据。都会检查数据是否存在。造成mysql性能开销。

锁问题:锁竞争激烈,可能会导致死锁

不适合分库分表:外健难以跨越不同数据库简历关系。

mysql中为什么会选错索引/如何解决?

索引等选择是 基础性/区分度越高。优化器就会选择该条/ 选择性。能够删除掉更多的数据。索引覆盖/orderby。优化器为了避免filesort。

优化器是基于不确定性的数据进行优化,这些数据可能过时。

关联表复杂,代码难以直接判断。

唯一索引和主键索引的区别?

唯一性二者都是一致的。主键索引不能为空/唯一索引可以存在null。主键索引只能是一个,唯一索引可以存在多个,主键索引肯定是聚簇索引,可以被引用外健。唯一索引通常为非聚簇的

mysql中一条查询语句执行顺序

from 确定查询的表 join 确定关联的表

where进行条件筛选。group by 进行聚合 having 对group by 进行过滤。然后 select 查询出列。disct对列进行过滤

order by 进行排序 ,limit输出多少列

mysql查询执行时间

8.0以前,使用shoe profile字段。

8.0新增analyse 字段

mysql中on 和where

on都是用于制定条件

on用于指定2个表如何关联,where是如何过滤数据。on在前,where在join之后

mysql中什么情况下会导致不自增

删除数据/手动制定id/事务回滚/手工修改自增数值/insert for update dupkey

mysql中如何优化filesort

这里得讲一下如何进行order by排序。

就是磁盘辅助排序,情况就是构建符合索引/提高临时排队的内存大小,设置一下参数

mysql中什么是索引合并?

索引合并就是 对多个索引分别查询出来的结果进行合并。

直接使用explain 查看extra字段。有index_merge就是索引合并

当使用多个and的时候,优化器可能会使用合并算法/

当使用多个or/union时候 交集算法。

mysql中undolog会一直存在吗?

不会。有个purge线程。

当一个事务 在 所有活跃线程读事务之前,那么这个事务对应undolog就可以删除,因为不需要它的快照信息进行回滚了

mysql中limit原理

通过s q l执行顺序可以看到。limit是最后执行的。所以是先查询出结果。然后在丢弃数据。这样子也会导出深度分页问题

mysql中为啥不实用select *

数据会使用索引覆盖进行优化/减少回表

增加io/网络负担

Redis

Redis中是AP还是CP的

redis主从节点使用的是异步复制。当一个节点挂掉的时候,会有部分数据没有同步过去

Redis集群

首先是主从复制。一个主节点,负责写,几个从节点,读取读取。适合读多写少的场景。缺点就是当主节点挂掉。从节点不能给主动升级为主节点,需要手工,不能故障转移

哨兵模式。就是在主从节点上新增了多个哨兵节点,当多少哨兵探测到主下线,则开始让其中一个节点变成主节点。

cluster节点。采用了数据分片的模式。使用1.6w个哈希曹。每个哈希曹分配给多个分布式节点。每个节点都是主从模式/每个节点负责多个哈希曹。当有数据访问时。先对key进行 运算,找到对应的节点,直接开始访问数据。cluster节点探测到 分片节点不可用时,会自动从子节点中选出一个

Redis协议

基于tcp的resp协议,简单文本协议。

bash
*参数的个数\r\n
$参数的长度\r\n
参数\r\n

Redis和memcached区别

数据结构不同:redis支持多种数据结构,hash,string,set。zset. m只支持简单键值对存储

持久化方式不同:redis支持rdb,aof持久化方式。m没有持久化

处理数据方式不同:redis单线程操作/支持事务/lua。m是多线程,并且支持get,set

redis为啥这么快?

基于内存的。内存速度比磁盘快很多

单线程,所有数据在单线程完成。不需要上下文切换即可完成。

io多路复用,虽然是单线程。但是利用io多路复用技术,能够提高处理请求能力

高效的数据结构,设计很多数据,底层设计很完善,能够在o1的情况下完成数据的读写

redis6.0中,引入了多线程,这里多线程只是对网络请求变成多线程,能够在io多路复用的基础上,性能进一步提升。写数据单线程

redis支持哪几种数据结构

string hash,list,set,zset,bitmap。hyperlog,geo

redis为啥自己定义SDS

sds就是简单动态字符串,redis是用c语言实现的。c语言的字符串是用 通过\o来判断字符串结果。redis中使用太广泛了,需要对其进行操作的话,每次都需要从头遍历。不方便,所以自己封装了sds。里面数据字符串数据/len长度,分配给该字符串 allocate总长度字段。操作更快

Redis中zset是怎么实现的

redis7.0之前是 ziplist和skiplist, 7.0之后是listpack+skiplist。首先讲一下共有skiplist.

skiplist就是跳表,它在链表多层次索引。这样子可以让插入/删除/修改 都是logn的效率。但是缺点,一个节点有个索引,内存消耗是比较大的。

ziplist小数据量的存储。是一种非常紧凑的数据,里面包含了头节点,偏移量,数据总数,entry节点,entry节点中记录了前一个节点的数据长度。但是有新节点插入进来,导致后续entry长度发生变化的话,就会让后续所有记录前一个节点长度的值进行更改,就是集联更新问题。查询o(n)。后续就用listpack进行了代替。

当里面的数据超过一定的阈值时,就会将ziplist中的数据转换成 skiplist.这个参数时是可以设置的

Redis中什么是geo

主要是从来存储经纬度信息,如果需要查询附近的人,离最近的地点。使用这个就非常方便

使用geoadd 先添加经纬度信息。 georadius命令,查询半径内多少多少人名称距离。

redis为啥是单线程

redis中有很多模块,数据操作模块,持久化模块,网络请求模块。redis中只有数据操作模块是单线程的。

redis是基于内存,并没有cpu资源性能瓶颈。影响性能瓶颈。大部分只有io操作,有网络io/磁盘io。

使用多线程没有必要,而且多线程还对额外的锁开销,/而且单线程避免多线程切换资源开销。

网络io/磁盘io目前redis 7.0以后都是使用的多线程。

为啥lua脚本保证原子性

并发编程中:原子性就是 操作不可被中断/操作不可拆分。跟acid中有些区别,要求,事务要么被执行,要么被回滚。

redis是单线程,当执行lua脚本时,不可以被其他命令打算。所以redis保证的是以原子性来执行lua代码程序,但是不能保证lua脚本执行时发生错误后,发生回滚/全部执行

什么是stream

Group create创建消费组。xack确认处理。xreadgroup 读取消费组 xinfo stream打印流信息。

xread读取消息。

stream数据结构可以被看成一个日志/消息队列。里面记录了消息的唯一id,时间磋,消息体

redis 5.0之前,用户可以通过发布/订阅 实现消息队列,缺点就是没有持久化。

Stream-支持持久化/有序消费,根据时间进行排列/消息分组/多消费者支持

rdb和aof联系和区别?

rdb就是定期保存全量快照。优点就是 恢复数据快。体积小。缺点,定期更新,会丢掉一段时间内的所有数据

aof就是通过追加写的方式,把数据写入到日志中,优点:粒度更细,数据更加可靠,缺点:回复速度慢。体积大,当日志提交超过一部分,则需要合并

redis4.0以后可以支持混合写。会讲rdb格式格式写到aof文件开头,继续追加写,缺点就是不能给向下兼容。

redis中的事务

redis中的事务主要是保证事务的执行不会被打断。并不会提供事务的回滚操作。

当命令排队,检查时已经错误,调用exec会直接报错。如果执行过程中报错,会继续执行剩下命令

主要有几个命令:multi命令开始。DISCARD事务结束,exec执行,watch。观察key发生变化。unwatch

redis的过期策略是什么?

redis设置过期时间来控制key的生命周期。主要通过 惰性删除/定期删除 策略相结合。默认是2个都开启。

惰性删除:当有程序来访问时,会先判断key是否过期。如果过期则删除

定期删除:程序每100毫秒,随机抽取一部分数据,查看key是否过期,过期则删除

redis中的内存淘汰策略

redis中内存淘汰策略。

不淘汰任何数据,直接报错.allkey-lru。最近最少使用直接淘汰。volatie-lru.对设置了过期时间的数据,最近最少使用redis进行删除。allkey-randoms所有数据随机删除。volatile-random-所有数据随机删除。lfu是访问频率最低进行删除,volite-lfu访问频率最低的进行删除

Redis中只要那些缓存算法吗?

lru-最近最少使用

使用链表。新数据放在链表头部。访问数据时,将数据放在头部。删除数据,将链表尾部数据删除

lfu访问频率最低。是用队列实现,内部维护一个计数器。当数据数据被访问,则计数器+1。重新进行排列。

当数据新增,被添加在尾部。

Redis中热点key

首先就是需要定义什么是热点key。得根据不同业务设定不同的规则,比如qps到达多少就算热点。

一般做法就是识别热点key-》根据规则提前预热/实时统计-〉多级缓存-》热点拆分。比如淄博烧烤_001。

淄博烧烤_002。比如根据用户规则,把数据分散到不同地方

redis中什么是大key

一种是value比较多,另一种就是key比较大。

识别:当string 超过5mb/list超过 10000/hash查过10000/set超过1000都基本上算做大key。根据规则要么自己写程序扫描/要么redis-cli

解决:方式的话,进行二级拆分/根据业务字段,时间/门店id/ 如果是 string类型 进行压缩/对这些大key设置过期时。

Redis中缓存穿透/击穿/雪崩

穿透。造不存在的数据/大量访问,这样子就可以绕过redis。数据库中也没有,这样子就会导致数据奔溃

击穿:某一个key失效,导致大量数据访问到数据。异步更新/对key进行续期

雪崩:就是同一时间断,大量key失效。导致数据中不存在数据。随机ttl

穿透:redis中放null值/或者布隆过滤器

什么是布隆过滤器

是一种数据结构,判断数据是否一定不存在另一个数据中。

原理就是底层 是bit数组,当数据进过多成hash。分列在不同的 位上,将位上的值设置为1。

当查询时,同样的原理,判断bit数组上位是否都为1.都为1说明可能存在,否则肯定不存在。因为有hash冲突,所以是可能存在。

主要用于网页爬虫/缓存。

Redis中什么情况下会发生数据库和缓存不一致的情况?

写数据和写缓存一个成功一个失败

写缓存并发。一个线程在另一个线程操作时内发生

读写缓存并发:写缓存在读操作中间

redis和数据库的一致性问题

  • 先删除数据库,再删除缓存
  • 延迟双删。时间不好掌控
  • Canel。cache-aside

redis如何实现延迟消息

redis5.0之前通过过期监听的一个功能实现。

但是redis删除数据并不是立刻删除的,所以消息可能会滞后,而且不持久化

也可以使用zset。将数据+过期时间房间z set中,开启扫描表任何。当过期时间小于当前的时候任务,都取出来进行操作。

使用redesign。将上述的操作封装成一个内部的延迟队列

redis出了做缓存,还能做什么?

分布式锁/排行榜(热门排行榜,热门商品列表)/计数器/分布式id/分布式限流/分布式session/状态统计/共同关注

redis如何用setnx实现分布式锁

由于单线程的缘故,只有一个线程能够获取到,其他线程则会返回false。搭配过期时间,防止获取到锁的用户宕机,导致锁一直无法释放。当释放锁时,会判断锁是否数据当前客户端,如果属于,则进行释放

Redession 帮我们封装了一层,使用看门狗机制,完成获取锁,续期等操作。

redission实现分布式锁?

为了避免锁超时,reDISSON中引入了看门狗机制,帮助我们再redisson实例被关闭钱,不断的延长锁有效期。

Redis中zset能够支持高效的范围查询,还能以0(1)复杂度获取数据

因为zset底层结构是2个部分组成,一个是hash表字典,一个是跳表。跳表就是多层级索引链表,支持范围查询

zset查第5~10的指令怎么写?

ZRANGE key 4 9 WITHSCORES

Redis中什么是看门狗

自动续住自动续住redisson客户端实例。当获取到锁时,会开启一个netty时间轮任务,每10秒钟对数据重新设置过期时间。过期时间没有设置的话,一般为30秒。保证任务时间很长,也不会过期

如果用户释放锁/客户端链接关闭,会自动停止续租任务。

redis中什么是rehash

hash表中内部维护了2个hash table。hashtable1和hashtable2。新增数据一直都会写入hashtable1。当出发扩容,会维护一个rehashindex=-1每次再新增数据的时候,会把hashtable1中 对应的数据 放到table2中。rehashindex+1。知道所有数据扩容完成,则2个表的指针互换

Redis节点

网络分区;一部分的 哨兵节点链接不上主节点,就会重新从身下可以使用从节点选出一个主节点

主节点恢复:主节点宕机,然后又很快恢复,一部分哨兵需要重新选举,一部分认为不需要

Redis实现滑动窗口限流

定义一个窗口大小,可以设置为60妙

每次请求将时间戳放在zset中。然后通过zremrangebysocre命令来删除小于窗口时间的数据。

然后通过zcard统计数据大小。看下是否超过阈值,如果超过则返回。

redis中key和value有啥设计原则

key最好以命名空间为准,避免冲突

避免太长/避免特殊字符串/有业务含义

value选择合适的数据结构/避免大key/合适的过期时间/能压缩就压缩

redis中pipeline和事物有啥区别?

pipeline是优化网络延迟的一种手段,一次性多个命令给客户端,这些命令是被单独执行的。但是不保证一整批命令不可分割,被打算。跟事务不一样。事务能够保证一整批命令,没有其他命令进行插入

lua和事物之间的区别

lua和事物都能够保证原子性,但是有一些差别。lua其中一行运行失败,后续代码不会执行。事务,一行命令执行失败,后续会继续执行

交互次数,redis在收到exec命令之前,会把之前提交的命令缓存起来。交互次数比较多。lua只需要一次。

前后依赖.lua能够依赖上一个输出的结果进行下一步的操作,事务是不支持获取上一行命令返回的结果的

流程控制:lua能够自定义流程,事务不行。

trylock和lock之间的区别?

trylock就是非阻塞锁,当用户尝试获取锁时,如果获取不到,会等待waittime时间,然后还获取不大会false。

lock则是会一直尝试获取锁。只到成功,是一种阻塞锁

如果使用redis实现乐观锁

实现该功能,利用watch机制。监听某个数据是否发生变更

首先监听key。获取key值,开启事务,设置事务值,执行事务。

如果事务执行失败,则说明更新失败,因为如果事务开始执行中,key发生变更,会打断事务。事务就会执行失败。如果执行成功,说明更细 成功。

setnx如何实现可重入锁

获取锁key: value就是线程+count

redis实现分布式锁的时候,那些问题需要考虑?

互斥性:使用setnx。lua脚本 可重入性:对key的值设置重入次数。性能:基于内存,最快了

redis如何高效安全的遍历所有key

遍历key只有2种方法,第一种keys,能够一次性返回匹配的数据. Keys * 则返回所有数据。数据量很大的时候,会阻塞系统。

scan会返回一个游标方式。当游标数据返回0,则表示没有数据了

cluster中使用事务和lua有什么限制?

lua和事物一样,所有的相关键位都必须在统一个槽上。

watchdog什么时候会失效?没有自动续期

主动设置了超时时间/机器cpu打满/网络问题,连接不上redis

redisson如何保证解锁的线程一定是加锁的线程

设置锁的时候,会把加锁的线程id放在value字段,解锁的时候,先获取锁的内容,然后锁的id是否一致,如果一致,则进行解锁,如果不一样,则失败

rdb和aof写回策略分别是什么?

rdb通过设置save字段。 Save 900 1 每900秒有一个键发生修改,则触发快照保存

aof有三种策略。第一种always,直接写回磁盘。every sec。就是先写回内存缓冲区,然后每一秒写回磁盘

然后no。就是直接写入内存缓冲区,有操作系统控制数据写入磁盘

redis能完全保证数据不丢失吗?

不能。redis是基于内存,虽然有缓存rdb和aof持久化机制。但是redis进程异常,服务器断电,内存数据可能会丢失

redis事务和mysql区别?

redis中是简单的事务,通过提供multi,exec,discard,watch等命令,保证一组命令顺序执行,中间不会被其他命令打断,但是不支持事务回滚,其中一条命令出现问题,还会继续执行。也没有隔离级别。redis事务简单,轻量。为了性能,抛弃了数据的强一致性

redis中HASH结构比string的好处有哪些?

Hash结构修改数据中某个字段的信息非常快,string全部查询出来,修改完,再全部写回

redis 8.0有新特性

开源协议变更,编程APPLv3

增加了几个数据结构 vector set主要是为了支持ai。原生json支持。水岸序列。增加了概率数据结构:布隆过滤器/top-k/t-digest

性能提升:

优化io多线程/命令执行加速/主从复制优化/

Redis中setnx和setex

只需要 sent 不存在键值的时候,才设置值

setex.设置值并制定过期时间

Redis中为啥zset和ziplist进行转换

这个是基于内存优化/性能考虑的 ziplist,是一个紧凑结构,数据量很小的是你,查询复杂度虽然o(n)。但是数据量很小,所以性能很快。skiplist虽然查询logn,但是需要多余指针,浪费存储空间

Redis中listpack如何解决级联更新的问题

listpack抛弃了prelen。使用backlen记录整个entry长度,并且位于元素末尾。只记录了自己的长度,而不是上一个节点长度。当新增数据/删除元素,只影响元素自己本身

了解redis中的内存碎片吗?

redis的内存碎片形成的主要原因是内存在分配的时候,没有做到精致按需分配。比如jemolloc就是先申请,在使用

redis有后台程序,专门清理内存碎片,对数据进行移动/复制/释放旧内存

hash和java中hashmap是

ziplist+hashtable

hashmap则是数组+链表+红黑树

ziplist非常紧凑,适用于小数据量

安全性

二者扩容方式不一样:hash是渐进式扩容。缺点数据内存翻倍

kafka

多种mq有啥区别

kakfa跟其他2种有啥区别。

消息模型:kafka主要支持发布/订阅模式,而mq支持点对点(推模式)/发布订阅(拉模式。

rocketmq支持优先级队列,死信队列,延迟队列,支持事务模式。重试队列。

而kafka基本上不支持。延迟队列可以间接实现。

kafka整体的吞吐量是最高,能够处理海量的数据。能够更快持久化。高效的信息查询。

开发部署的角度:kafka部署比较简单,使用简单。

kafka为啥这么快?

可以从三个角度进行分析,消息发送/消息存储/消息消费

消息发送:批量发送。支持一次性发送多条信息/异步处理消息/数据压缩/并行发送,将数据发送到多个分区

消息存储:零拷贝-存储信息速度很快/顺序写入,消息是按照磁盘顺序写入/页缓存,利用空间局部性原理,把一整页加载到缓存/稀疏索引/分区和副本信息,实现分布式存储

消息消费:使用消费组,实现负载均衡/批量拉取消息进行消费/并行消费,不同消费者可以消费不同分区的内容

kafka中的架构

主要包括三部分,生产端/kafka集群/消费端

生产端可以向一个或者多个topic发送消息。

kafka主要是有个broker节点组成,一个broker中存在一个或者多个topic的分区副本。

kafa中有几个概念,第一个topic主要是用来区分业务逻辑的。一个topic包含多个分区。每个分区中又有leader和follow之分,leader负责处理生产端/消费端的消息。follow只是leader的副本。还有zookeeper主要负责集群的状态管理和元数据。

消费端,也有多个消费组,一个消费组有多个消费者

kafka如何保证消息不丢失

三个步骤进行讲述

第一个就是 生产者发送消息到kafka

生产端配置了ack=0或者1。0就是消息发送不管了,ack=1就是本地日志就直接返回成功,如果FOLLOWER没有来得及同步,leader宕机就有可能导致数据丢失。acks=all能够保证消息被写入多个副本。isr确认后,生产端才认为发送成功。ack确认机制,如果用户配置不正确,也会丢失数据

第二步:就是kafa持久化数据

第一个持久化,将数据写入到磁盘中

第二个就是lsr复制机制,只有当多个副本被存储才确认消息已经被接受到

kakfa主要依靠副本机制,kafaka一个主题可以分为多个分区,每个分区可以有多个副本,分布在不同broker。

第三个就是 消费端:保证消息正确/正常提交偏移量

消费者端 错误的使用了自动提交偏移量。消费者默认会定期自动提交已消费信息的偏移量。如果拉取到数据后自动提交,消费者程序崩溃,则有部分数据未消费。建议关闭自动提交偏移量,手动提交。

kafka怎么保证消费只消费一次

首先:消费端必须加入一个消费组,同一个消费组内的消费者是共享负载的,(这里是由分区独占性保证的,一个分区同一时间只能有一个消费者)如果消息被其中一个消费端消费了,另一个消费者就不会接受到消息

用户可以手动提交偏移量,来跟踪消息,防止重复消费。

在业务上最好能够做到幂等。或者借助kafka中exactly-once机制

kafka的重平衡机制

重平衡就是 当消费者用户出现加入/删除的时候,需要将消费者重新分配分区。目的是实现消费的高可用和负载均衡

有很多情况可以触发:消费者端游有用户加入或者删除。订阅的topic数量增加,订阅的分区会增加

其他异常情况,broker宕机,leader宕机。都会触发重平衡

平衡的一个步骤:

第一步:暂停消费,/第二步计算分配方案,根据当前组的消费者数量以及分区数量,重新计算每个消费者的需要消费的分区列表/通知消费端重新加入消费组/第四步:实现分配/第五步:恢复消费。允许消费端拉取自己分区的数据。

kafka的顺序消费

一个topic有很多分区。kafka只能保证同一个分区是顺序消费的。

如果想要实现,只有2种情况,第一个只创建一个分区,或者用户只往一个分区发送数据

kakfa有几种选举过程

第一个是分区leader选择

所有分区会向 zookeeper中临时阶段注册自己的id。表示他们正在参数leader选择。

所以写入成功的副本会在zookeeper上创建一个序列号节点,并将序列号写入该节点。

节点序列号节点的最小值为leader。并将自己信息写入zookeeper中leader节点

第二个就是controller节点

controller 是kafka中的核心节点,负责分区分配,主题创建/元数据管理,同一时间段只能有一个broker为controller

新版本中依赖raft算法,旧版本的话。

大家一起尝试创建controller临时节点。创建成功,则为controller节点,管理。其他注册失败的则注册监听controller节点的事件。当监听到controller节点消失,则会立刻尝试进行创建controller临时节点。

raft;一开始大家都是跟随者状态/如果跟随者在150-300ms内没有收到任何领导发送的消息就会认为领导宕机,就会变成候选者状态,增加自己的任期号-》给自己投票。给其他所有机器发送消息。

选举有三种情况,第一种,选举成功,获得半数候选者投票。新的领导者,开始向所有机器发送rpc。宣布自己已经成为领导者,并阻止选举,第二个输掉选举,在等待候选者收到其他节点的rpc后,发现有领导者,则自己退回跟随者状态。选举超时,如果票被瓜分,则自增任号,发起新的一轮选举,为了避免瓜分的情况,raft给每个节点设置了不同时间的选举超时时间。

日志提交:

kafka消息发送的过程

kafka发送消息的过程主要是有3个程序组成。Main线程,send线程,还有一个累加器。

Main线程主要包括 拦截器/序列化/分区器。序列化完,知道向哪个分区发送数据,然后数据会放到累加器,等条件满足。send线程就会将数据发送给kafka。然后根据配置就可以知道是否等待。如果配置为0,不等待,直接发送,如果1或者-1,则需要等待kafka返回ack。也是异步回掉。如果发送失败。数据会继续放在累加器中,等待发送

kafka中的高水位

高水位是kafka中一个比较重要的概念。

高水位是已提交消息的最高偏移量。意味着所有副本都存在该条数据。

消费进度管理,消费者可以通过记录上一次消费的偏移量,然后与高水位节点进行相比较。来确定自己的消费进度。

消息的可靠性,主要数据都写主副本,并且被所有的同步副本确人,才会被认定为已提交消息,消费者无法消费高水位之后的数据

kafka为啥有了topic还需要分区

topic是业务逻辑上的一个区分,分区是物理意义上消息分区,一个topic拥有多个分区。可以提高吞吐量/拓展型/负载均衡。

吞吐量:一个topic拥有多个分区可以让系统并行的消费。每个partition可以通过不同的消费组进行独立消费。

负载均衡,当有新的消费者加入/离开时,可以通过重平衡,实现负载均衡

扩展性:当数据量增多,可以通过增加分区的方式,来增大存储容量和并发能力。

什么是kafka的isr机制

isr就是同步副本的意思。是kakfa保证数据库一致性和可靠性的保障。

isr列表中保存着leader和副本状态信息。当数据发送到leader中。leader中的数据会复制给所有的副本节点,只有副本节点确认了消息,才会认为消息被接受。

kafka0.9之前,有核心参数,如果follwer消息落后leader超过这个消息量,则会从isr集合中剔除,有个风险,就是当有瞬发流量时,所有follow节点都会被剔除

kafka0.9之前,认为只要在限定的时间范围内追上主节点,就认为可以在isr列表中

kafka支持事务消息吗?如何实现的?

kafka中的消息,只支持发送的消息的事务,保证一批次的消息,要么都失败,要么都成功。

kakfa需要事务协调器和事务日志,来完成。事务协调器会给每个生产者维护一个事务id。

客户端首先开启事务,事务协调器会发送在事务日志中记录事务的id。发送消息,发送消息给通知事务协调器自己发送的分区和主题。提交事务的时候,事务协调器会讲消息 变更已提交状态。如果回滚,则会让消息变成中止状态,消费者端只能消费已提交消息。

kafka为啥要依赖zookeeper?

kakfa负责管理broker状态管理,当有broker加入或者推出时。都会通知其他节点更新broker集群信息

分区信息:zk节点中存储着分区的元素,包括所有副本节点信息,谁是leader,谁是follower,isr列表

controller选举。

kakfa4.0发布kraft算法。主要是为了减少依赖,简化运维,提高扩展性,大规模kafka部署时,zk就成为了性能瓶颈。加快故障恢复:能够更快的选举出controller

kafka的渐进式平衡

重平衡是需要暂停所有分区的消费。然后重新进行分配,然后再连接。

渐进式重平衡,就是kafka计算最小释放的方案。实现部分释放。

第二部:重新分配,对部分释放出来的数据进行平衡。

批量消费如何确保消息不丢

2步:第一步;关闭自动提交。第二步:就是判断批量消费的数据里面任意一条出现失败,则不提交偏移量,但是需要做好业务幂等,避免消息重投带来的问题。

kafka的数据存储结构

不同分区逻辑上就是不同的文件夹,同一个分区下,会讲文件拆分成相同大小的segment。还有附带了offset和timeindex2个索引文件。

在写入时,采用顺序写的方式,把数据追加在文件后面,这也是kafka快的一个原因。

读取数据时,根据offet大致查找到文件位置,然后把文件读物出来,仔细到具体位置,开始进行消费。

zookeeper

zookeeper和redis分布式锁的区别?

  • ZooKeeper 分布式锁:基于 临时顺序节点Watcher 机制,实现了强一致性的、公平的锁。它更可靠,但性能开销相对较大。

    无死锁:临时节点保证了客户端宕机锁必释放。公平有序:顺序节点保证了先来后到的公平性。可靠一致:强一致性模型保证了锁的可靠性。

    缺点:性能不如 Redis,尤其在锁竞争激烈时,频繁的创建节点和 Watcher 通知会带来压力。

  • Redis 分布式锁:通常基于 SETNX 命令和 过期时间,是 AP 系统 下的锁。它性能极高,但在某些极端场景下可能存在可靠性问题(需要通过 Redlock 等算法来缓解)。

    • 客户端A获取锁(30秒过期),但业务执行了40秒。在第30秒时,锁被 Redis 自动释放,客户端B获取了锁。此时系统中有两个客户端同时持有锁。

      解决方案:使用 “看门狗”机制,在持有锁期间,另起一个线程定期(比如每10秒)对锁进行续期。

    • 主从切换导致锁丢失

      使用 Redlock 算法。该算法要求客户端在超过半数的 Redis 节点上依次获取锁,才算最终成功。

zookeeper如何进行节点选举。

首先讲一下zookeeper的角色,以及几种状态

Leader:①、整个 Zookeeper 集群工作机制中的核心,过选举产生的集群领导者,提供读写服务;②、一个 Zookeeper 集群中同一时间只能有一个实际工作的 Leader,它用来维护各个 Follow 与 Observer 之间的心跳;③、Leader 是事务请求的唯一调度和处理者,Follow 接收到事务请求会将请求转发给 Leader 处理。

  • Follow:①、Follow 只提供读服务,即只处理非事务请求,它接收到事务请求会转发给 Leader 服务器;②、它参与 Leader 的选举,参与事务请求 Proposal 的投票;③、一个 Zookeeper 集群同时可以有多个 Follow。
  • Observer:①、功能和 Follow 基本一致,提供读服务,即只处理非事务请求,唯一的差别是不参与任何投票。
  1. LOOKING:竞选状态,寻找 Leader。当服务器处于该状态时,它会认为当前服务器没有 Leader,因此需要进入 Leader 选举状态。
  2. FOLLOWING:跟随者状态。表明当前服务器角色是 Follower。
  3. LEADING:领导者状态。表明当前服务器角色是 Leader。
  4. OBSERVING:观察者状态。表明当前服务器角色是 Observer。

ZooKeeper 事务 ID (ZXID):这是一个单调递增的 64 位数字,代表一个事务提案。ZXID 越大,表示数据越新。它是选举中的重要评判标准。

myid:每个服务器在 dataDir 目录下的 myid 文件中配置的一个数字,用于唯一标识集群中的一台服务器。

下面开始流程讲解

  1. 初始状态 (LOOKING):所有服务器启动,初始状态都是 LOOKING
  2. 第一轮投票:投给自己
    • 每个服务器都首先投票给自己。投票内容为:(epoch, ZXID, myid)
    • 由于是集群启动,ZXID 都是 0。假设 epoch 都是 1。
    • Server1 的投票: (1, 0, 1)
    • Server2 的投票: (1, 0, 2)
    • Server3 的投票: (1, 0, 3)
    • 它们各自将投票放入自己的投票箱,并将投票广播给集群中的所有其他服务器。

3.接受投票与更新逻辑

  • 每个服务器都会接收到来自其他服务器的投票。核心操作:当一台服务器收到一个投票时,它会将这个投票与自己当前所持有的投票进行比较。
  • 比较规则(如上所述):
    • 如果收到的投票的 epoch 更大,则认同该投票,更新自己的投票并重新广播。
    • 如果 epoch 相同,但收到的投票的 ZXID 更大,则认同该投票。
    • 如果 epochZXID 都相同,但收到的投票的 myid 更大,则认同该投票。

4。票数统计与状态变更

  • 在这个过程中,所有服务器的投票会逐渐收敛到同一个候选人上,即 (1,0,3)
  • 当一台服务器发现,超过半数的服务器都投票给了同一个候选人(在这里是 (1,0,3)),选举就结束了。
  • Server3 发现自己就是被选中的 Leader,将自己的状态从 LOOKING 改为 LEADING
  • Server1 和 Server2 发现自己不是 Leader,并且知道了 Leader 是 Server3,它们将自己的状态从 LOOKING 改为 FOLLOWING

5.建立通信

  • Follower 与 Leader 建立连接,并进行数据同步。
  • 集群正式进入正常工作状态。

zookeeper的典型应用场景

分布式配置管理,zk可以动态读取配置信息

分布式同步:协调各个节点的同步,确保数据的一致性

命名服务:

集群管理:zookeeper可以管理分布式集群,协调各个节点的加入和推出

master选举:zk可以用来实现master选举,选择一个节点作为master

分布式协调服务:zookeeper提供了一些分布式协调服务,比如分布式锁,唯一标识生成等

zookeeper数据目录结构是怎么样的?

zk中数据是以目录结构形式存储的,每一个存储数据的节点都叫做znode.每一个znode都有唯一的路径标识,主要有四种节点,持久节点,持久的连续节点,临时节点,临时的连续节点,当会话结束,临时节点也会被自动清除掉

zookeeper集群中的角色有哪些?

领导者:负责进行投票的发起和决议,为客户端提供读和写服务

跟随者:用于接受客户端请求并响应客户端返回结果,并参与投票

观察者:接受客户端连接,将写请求转发给leader。观察者不参与投票,只同步leader状态,observer的目的就是扩展系统,提高读取速度。

zookeeper的watch机制是如何工作的?

首先是2个模块一个watchMananger。这个服务端的模块,用于所有watcher注册。注销。触发操作

zkwatchmananger是客户端的模块,用于管理客户端所有watch创建,注册,处理watch事件。

首先客户端连接到服务器后,就会创建zkwatchmanager实例,用于客户端中所有watcher对象,当客户端想要监控某个node的时候,它就可以调用zkmanangermanger中的方法创建watcher并将其注册到客户端中,然后watcher信息发送给服务端,服务端接受到消息后,会将watcher信息交给watchmananger处理。watchmanager会将该watcher信息注册到对应znode节点上,当znode节点发送变化,watchmanager会通知服务端,服务端发送信息到客户端

ZOOKEEPER是如何保证创建的节点是唯一的

zookeeper中所有的写请求都是由leader节点完成,即使请求到follower节点,也会被转发到leader节点上执行。

leader节点写数据,会先加锁,保证只有一个线程添加成功

zk的缺点

zk的缺点就是性能问题,设计就是为高一致性,zab协议需要同步写操作到大多数节点,导致写操作到性能较低,高并发写入场景下,zk的性能会成为瓶颈。

zk中所有的操作,都必须经过leader节点,如果leader挂了,直到选出新的leader之前,都无法正常工作

zk的数据存储都在内存中,以提高快速访问性能,意味着不适合存储大量数据,只能一些元数据,数据量过大,会导致内存不足。

如果节点变更频繁,会触犯触发leader选举,影响使用

如何使用zk实现分布式锁

每个客户端对某个方法进行加锁,zk则对其创建一个节点目录,生成唯一的临时有序节点,判断是否获取锁的方式也很简单,只需要判断有序节点中序号最小的一个,当释放锁时,删除临时节点即可。

一旦客户获取锁之后突然挂掉,那么这个临时节点就会删除,其他客户端就会再次获取到锁

可以实现非阻塞锁。创建临时有序节点。并创建监听器,如果节点发生变化,则通知客户端,客户端检查自己创建节点是否是当前所有节点中序号最小的,如果是则获取到锁。

可重入:把自己的线程信息写入节点信息中就可以实现

Dubbo

Dubbo同步 调用?

  • 将请求封装为Request对象,并构建DefaultFuture对象,请求ID和Future对应。
  • 通过Netty发送Request对象,并返回DefaultFuture对象。
  • 调用DefaultFuture.get()等待数据回传完成。
  • 服务端处理完成,Netty处理器接收到返回数据,通知到DefaultFuture对象。
  • get方法返回,获取到返回值。

如上代码,我们重点来看get方法。我们总结下它的运行流程:

  • 判断超时时间,小于0则设置默认值
  • 判断操作是否已完成,即response是否为空;如果已完成,获取返回值,并返回
  • 如果操作未完成,加锁、等待;获得通知后,再次判断操作是否完成。若完成,获取返回值,并返回。

Dubbo为啥比Http快?

其实,RPC的设计目的就是用于高效的内部服务通信,他通常优化了数据传输和序列化过程,目的是减少网络延 迟和提高性能。而HTTP的设计是一种更通用的协议,用于Web文档传输,它在灵活性和可访问性上进行了优化, 而不是仅仅专注于性能。展开来说主要在以下几个方面,RPC做出了很多事情。

轻量级序列化协议,RPC通常使用更高效的数据序列化格式(如ProtocolBuffers、Thrift等),这些格式专门为 性能和效率设计,它们比HTTP标准使用的文本格式(如JSON、XML)更紧凑、解析更快。

网络协议更优,RPC的网络通信协议通常被设计的更轻量(如Netty),他一般不需要像HTTP那样有很复杂的 Header信息,从而不需要传输太多的数据。

长连接,虽然RPC和HTTP都是基于TCP的,但是RPC可以使用长连接和更有效的连接管理策略,如gRPC还是基于 HTTP/2实现,这可以减少建立连接的开销,并允许多个请求在同一连接上有效地复用。虽然HTTP/1.1也有keep alive机制,HTTP/2也有很多优化。但是RPC只在企业内部用,所以兼容性更好,而HTTP新版的普及程度并不太 高。

定制优化,RPC框架通常允许更深层次的定制和优化,比如调整底层传输细节、序列化方式和错误处理机制。而 HTTP作为一个标准化的Web协议,其灵活性和定制能力可能较低,特别是在面向性能的场景中。

内部网络,RPC通常应用于企业内部,内部网络交互链路更短,而HTTP在公网上进行通信,一次交互需要经过多 个中间节点的转换。

Dubbo的SPI和JDK的SPI有什么区别?

在Java中,SPI(Service Provider Interface)是一个为软件设计的扩展机制,允许第三方为某些接口提供实现。 不同的框架和库可能会有不同的SPI机制,如JDK、Dubbo都支持。

1.懒加载vs预加载

JDK的ServiceLoader会在使用ServiceLoader.load()方法时加载所有可用的服务实现,这是一种预加载方式。

Dubbo的SPI支持懒加载,即只有当实际需要使用服务时,才会加载服务的实现。这可以提高应用启动速度,并减少资源消耗。

2.扩展点自动激活

JDK的SPI不支持根据条件自动选择和激活具体的实现

Dubbo的SPI支持扩展点自动激活功能。可以通过键值对的方式在配置文件中指定条件,当满足条件时自动 选择对应的实现。

3.扩展点自动装配

JDK的SPI不支持依赖注入,服务提供者需要自己管理依赖或使用其他方式注入依赖。Dubbo的SPI支持依赖注入。Dubbo可以自动注入扩展点所需的其他扩展点,使得开发者可以很方便地在 一个扩展实现中使用其他扩展服务。

4.AOP 支持 JDK 的SPI没有内建的方法来支持面向切面编程(AOP)。

Dubbo的SPI机制支持通过wrapper类包装扩展点,从而支持AOP风格的服务增强。这可以用于日志记 录、事务管理等。

JDKS PI机制虽然简单易用,但在灵活性和扩展性上有所不足。比如说JDK的SPI只能加载所有实现类的实例, 并不能对实例进行过滤或排序,缺乏对加载顺序、优先级等高级特性的支持。Dubbo的SPI支持扩展点自动激活 功能。可以通过键值对的方式在配置文件中指定条件,当满足条件时自动选择对应的实现。 JDKSPI机制在加载实现类时会扫描所有的服务提供者配置文件,这会导致性能问题。特别是在服务提供者较多 时,这种扫描和加载机制会带来额外的性能开销。 30

分布式

分布式和集群的区别?

分布式是针对集中式来说的,集中式就是把所有功能都部署在一起。导致难以维护/系统庞大/容易出现单点故障

分布式就是将集中进行拆分,变成多个系统。整个分布式系统对外提供一整套服务。

分布式就是:多台不同服务器部署不同的服务模块,通过网络通信进行连接。对外提供服务

集群就是:多台不同机器部署相同的模块,通过负载均衡对外提供

什么是分布式一致性

所谓一致性,就是多个副本之间,数据是否能够保持一致

强一致性:系统保证每个读操作返回的最近写操作的结果,在任何时间点,客户端都能看到相同的数据视图,包括线性一致性,顺序一致性

弱一致性:允许不同节点之间的数据访问存在一定程度的不一致性,以换取更高的性能和可用性。

最终一致性模式:允许系统在发生分区/网络故障后,经过一段时间,系统最终达到一致状态。

线性和顺序主要差别在于强调实时性,线性一致性要求操作在实际时间上顺序保持一致,而顺序一致性只要求保证操作的顺序一致的,不一定要求操作的实际时间。

什么是CAP。为什么不能同时满足?

一致性:每次读取都会收到最新的写入数据或者错误信息

可用性:每次请求请求都会收到非错误响应。但不能保证响应都是最新的数据

分区容错性:分布式系统遇到某个节点或网络分区故障的时候,系统仍然能够继续运行

首先讲一个前提,分布式系统,网络分区是前提,所以我们只需要论证 在p的前提下,为啥无法同时满足a和c

比如现在有2台机器,a和b。里面都存储一个数据v0.a和b网络断开,如果用户将数据修改v1.另一个用户来访问数据,此时返回的数据是v0还是v1。保证可用就只能返回v0的数据,是旧数据,如果牺牲可用,等网络恢复,那么用户拿到的就是最新的数据v1

什么是分布式Base理论?

base理论就是在cap基础上的一个拓展

base就是 基本可用+软状态+最终一致性

既然无法做法强一致性,那就是最终一致性,无法100%可用,就是基本可用

基本可用:分布式系统中出现故障,允许损失部分可用性,保证核心可用。比如电商流量激增,会进行服务降级

软状态:就是允许系统出现中间状态,而中间状态不会影响系统整体可用,比如分布式存储中,一般会有几个副本。比如初始状态,处理中,不断进行重试,最终成功

最终一致性:是指系统中所有数据副本经过一定时间后,最终达到一致的状态

分布式2阶段提交2pc?

就是把所有节点分为2类,一类是协调者,一般只有1个,一类是参与者。

协调者向参与发送准备消息,询问是否提交事务。参与者:有2中,执行事务,将日志变成预提交状态。

如果成功,则返回yes,如果失败,返回no。

二阶段,提交阶段。协调者等待所有参与者返回消息,如果都是成功,yes,则提交事务,发送commit。如果任何一条失败,则返回失败,进行回滚。

分布式锁有几种实现方式?

通过数据库,redis,zookeeper。

数据就是悲观锁,redis就是setnx, zk就是通过创建的临时有序节点实现

什么是分布式事务?

是指分布式系统中设计到多个数据库或者多个应用程序,而这些程序或者数据在不同物理节点上,需要确保所有的参与者事务要么全部提交成功,要么全部回滚。

为了解决这些分布式问题,出现解决方案。比如XA协议,tcc事务,最大努力通知。不同的协议最终实现的方案不同。

常见的分布式事务

XA协议 2pc。强一致性的,性能较差

TCC最终一致性,业务请入大。

Saga最终一致性。异步/非阻塞,适合长事务

本地消息表:最终一致性,

mq如何发送事务消息

一般是 ro cketmq

事务的发起方执行本地流程

事务的发起方向事务的参数放发送mq消息

事务的参与放接收到mq消息后执行本地事务。

什么是TCC,和2pc有啥区别?

tcc是一种分布式事务解决方案,采用基于业务逻辑的补偿机制,将整个分布式事务分解为若干个子事务,每个子事务都有try confirm和cancel。

try就是就是执行本地事务,执行成功返回一个标识。如果所有参与者都执行成功,就会执行confirm阶段。

cancel就是任何一个参与者在try节点失败,就会通知所有参数者进行回滚操作,

缺点:代码入侵程度较高。实现复杂。

存在悬悬挂事务问题,部分事务成功,部分事务失败。有些参与者 了,有些参与者失败了,这个时候就需要所有参与者都执行cancel。但是对于try失败的人来说,本次回滚就是一个空回滚,需要对业务中做好空回滚 的操作。

tcc中,confirm或者cancel失败怎么办?

一般常见的操作就是重试confirm。

如果confirm失败的话,就需要执行cancel操作。

日志监控,人工干预

什么是柔性事务

染性事务,是业内解决分布式事务的主要方案。所谓柔性事务,相比绞与数据库事务中的ACID这种刚性事务来 说,柔性事务保证的是“基本可用,最终一致。”这其实就是基于BASE理论,保证数据的最终一致性。 虽然柔性事务并不像刚性事务那样完全遵循ACID,但是,也是部分遵循ACID的,简单看一下关于ACID四个属性, 柔性事务的支撑程度: 原子性:严格遵循 一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽 隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽 持久性:严格遵循 在业内,关于柔性事务,最主要的有以下三种类型:异步确保型、补偿型、最大努力通知型。

mq如何实现分布式事务

事务的发起方执行本地事务

事务的发起方向事务的参与方发送mq消息

事务的参与方接受到mq消息后执行自己的本地事务

基于本地消息表实现分布式事务

这个方案的主要思想是将分布式事务拆分为本地事务和消息事务两个部分,本地事务在本地数据库中进行提交或回 滚,而消思事务则将消息写入消息中间件中,以实现消息的可靠投递和顺序性。 一般来说的做法是,在发送消息之前,先创建一条本地消息,并且保证写本地业务数据的操作,和,写本地消息记 录的操作在同一个事务中。这样就能确保只要业务操作成功,本地消息一定可以写成功。

写本地业务表/写本地消息表。然后发送mq。mq读取消息,写本地业务表。

1、2如果失败,因为在同一个事务中,所以事务会回滚,3及以后的步骤都不会执行。数据是一致的。 3如果失败,那么就需要有一个定时任务,不断的扫描本地消思数据,对于未成功的消思进行重新投递。 4、5如果失败,则依象消息的重投机制,不断地重试。 6、7如果失败,那么就相当于两个分布式系统中的业务数据已经一致了,但是本地消息表的状态还是错的。这种 自 情况也可以借助定时任务继续里投消息,让下游幂等消费再重新更改消息状态,或者本系统也可以通过定时任务去 查询下游系统的状态,如果已经成功了,则直接推进消息状态即可。

分布式id

全局唯一:必须保证全局唯一性,这个是最基本的要求。 高性能&高可用:需要保证ID的生成是稳定且高效的。 递增:根据不同的业务情况,有的会要求生成的ID呈递增趋势,也有的要求必须单调递培(后一个ID必须比前 一个大),也有的没有严格要求。 通常,在分布式ID的生成方案主要有以下6种:UUID/数据库自增ID/号段模式/基于Redis 实现/雪花算法/第三方ID生成工具。

号段模式是在数据库的基础上,为了解决性能问题而产生的一种方案。他的意思就是每次去數据库中取ID的时候取 出来一批,并放在缓存中,然后下一次生成新ID的时候就从缓存中取。这一批用完了再去数据库中拿新的。

而为了防止多个实例之间发生冲突,需要采用号段的方式,即给每个客户端发放的时候按号段分开,如客户端A取 回 的号段是1-1000,客户端B取的是1001-2000,客户端C取的是2001-3000,当客户端A用完之后,再来取的时候 取到的是3001-4000。

雪花算法

雪花算法(Snowflake)是由Twitter研发的一种分布式ID生成算法,它可以生成全局唯一且递增的ID。它的核心思 想是将一个64位的ID划分成多个部分,每个部分都有不同的含义,包括时间戳、数据中心标识、机器标识和序列 号等。 具体来说,雪花算法生成的ID由以下几个部分组成:符号位(1bit):预留的符号位,始终为0,占用1位。时间戳 (41bit):精确到毫秒级别,41位的时间戳可以容纳的毫秒数是2的41次幂,一年所使用的毫秒数是:,算下来可以使用69年。3.数据中心标识(5bit):可以用来区分不同的数据中心。机器标识 (5bit):可以用来区分不同的机器。序列号(12bit):可以生成4096个不同的序列号。

所以,基于时间截+数据中心标识+机器标识+序列号,就保证了在不同进程中主键的不更复,在相同进程中主键的 有序性。 雪花算法之所以被广泛使用,主要是因为他有以下优点: 1.高性能高可用:生成时不依赖于数据库,完全在内存中生成。高吞吐:每秒钟能生成数百万的自增 ID。ID 自增:在单个进程中,生成的ID是自增的,可以用作数据库主键做范围查询。但是需要注意的是,在集群中是没办法保证一定顺序递增的。

缺点:基于zookeeper实现节点id生成,zk可能成为性能瓶颈,依赖于系统时间一致性,如果系统时间回拨,可能造成id重复。

如何实现分布式Session?

客户端存储,通过cookie把session信息发送过俩,存在安全问题

分布式存储:需要数据库,redis数据支持

session复制:把session同步给其他数据库。

seata4种事务模式,各自适合什么场景?

首先AT模式,它的优点就是没有侵入性,你只需要按照Seata的要求引入@GlobalTransaction注解,就可 以实现你的分布式事务了,他不需要做额外的操作,你只需要关注你的业务泛辑即可。但是他的局限性就是只能支持那种具有ACID属性的关系型数据库的操作,比如MySQL,因为他要基于日志 进行回滚。如果你的项目中,需要把写数据库和写Redis放到同一个分布式事务中,AT模式就不支持了。

其次是TCC模式,这种模式可以支持多数据源的情况,不管你是Redis、MySQL还是ES,反正他就是要你 自 自己实现Try、Confirm和Cancel,具体的没辑你自己写,提交、回滚的代码你自己来实现就行了,所以他 对代码有一定的侵入性。

还有就是Saga这种模式,它适合长事务,什么是长事务呢?就是那种你有外部交互的场景,比如你要调微 信支付,就可以用这种模式来管理这个分布式事务。 以上三种都是最终一致性,而XA模式这种就适合于你对一致性要求非常高的场景,只有他是一种强一致性 ◎ 模型。

扫描表任务

1、消息堆积扫表慢-sql-添加索引。使用多线程并发扫描表,/分段扫描。

2、集中式扫表会影响正常业务。扫描备用库 3、定时扫表存在延迟问题

如何解决幂等

一般分为2种,第一种是请求幂等/业务幂等。一般将的业务幂等。

请求幂等:每次请求,如果参数一样,结果也是一样的。

同一次业务请求,在拿到最终状态之后的每次请求,结果要保证一样,在没有拿到最终状态之前,每一次请求需要正常执行业务逻辑,直到推进到最终态。1锁,二判断(基于状态机/流水表/唯一索引),三更新数据进行持久化

Seata AT原理是什么?

Seata 是一个开源的分布式事务解决方案,旨在提供高性能和简单的事务管理服务,用于微服务架构。其中,AT 模式是 Seata 支持的一种事务模式,全称为 Auto-Commit Transaction,也就是自动提交事务模式。 它是一种无侵入性的事务模式,对于业务开发来说,不需要做代码上面的改造,就可以实现分布式事务。 Seata中包含了三个组件: •TC (Transaction Coordinator):事务协调器,负责管理全局事务的生命周期,包括开始事务、提交事务和回 滚事务。 •TM(Transaction Manager):事务管理器,定义事务的范围,负责开启和结束全局事务。 •RM (Resource Manager):资源管理器,管理资源对象,如数据库连接,负责资源的注册与释放。

AT 模式基于两阶段提交(2PC)协议进行工作,通过代理数据源的方式,使得本地事务(如數据库事务)与全局 事务(跨服务的事务)能够统一管理。

所谓代理数据源,其实就是在应用自己的Datasource之上做了一层代理,使得原本的JDBC Datasource变成 Seata DatasourceProxy,这样就可以在这层代理当中控制SQL语句的提交、回滚等操作。

事务的提交也是2阶段。一阶段

1、RM针对本次要执行的本地事务的SQL进行解析,得到SQL的类型、修改的表以及where条件等信息。 2、RM 根据 SQL 解析的结果,先进行一次查询,根据查询结果生成相应的 before image(变更前数据快照)。 3、执行SQL语句进行数据库变更。 4、再查询一次变更后的记录,作为after image(变更后的数据快照) 5、把before/after image 以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。 6、提交前,向 TC 注册分支,并申请表中本次需要修改的所有记录的排他锁。 7、将业务数据的更新和前面生成的 UNDO LOG 一并提交。 8、将本地事务的执行结果上报给TC。

这里依赖的完全是本地事务,基于本地事务的ACID的特性,可以保证业务数据和回滚日志 (before/after image) 可以在同一个本地事务中被提交。 二阶段 在一阶段,业务操作完成后,TM 向 TC发起提交请求。TC 会发起没票请求,询间所有的RM是否可以提交事 务。那么就会出现2种情况: 提交事务:如果所有的 RM 都同意提交,说明他们此时他们的本地事务都已经执行成功了,那么TC就可以释放该 全局事务的所有锁,然后异步调用RM清理Undo Log.

回滚事务:如果任— RM 投票否决或者出现放障,那么就要协调事务进行回滚。 1、通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。 2、会拿当前数据先跟afterlmage进行比较,如果一致,执行第三步。如果不一致则在比较一下当前数据是否和 beforelmage,如果一致性,说明未提交成功或者已经回滚了,则无需处理。如果不一致,那么说明有脏数据了, 需要抛出异常,人工处理。 3、根据 UNDO LOG 中的before image和业务 SQL的相关信息生成并执行回滚的语句。 4、执行SQL并提交本地事务。并把本地事务的执行结果上报给 TC。

Seata的AT模式和XA有什么区别?

所以,他们的主要区别就在于一阶段是否直接提交事务,Seata的AT模式中,为了提升性能,直接提交了事务,在 二阶段,需要回滚的话再执行回滚。而XA中,一阶段是只做资源占用,二阶段在进行回滚或者提交。 所以,AT的性能是明显高于XA的,因为在一阶段已经执行了实际的数据库操作,并不需要做资源的占用和锁定, 而二阶段也只是在失败的情况下再执行回滚,所以性能相对较高。

什么是号分段模式生成分布式id的原理和优点

号段模式是一种常见的分布式ID生成方案,主要用于生成全局唯一的ID。其核心思想是通过分配一定范围(段)的 ID,避免了频繁访问数据库,提高了性能。

号段模式通常通过一个中心服务(比如数据库)来生成ID段(一个区间)。该中心服务负责生成多个ID段,每 个ID段具有固定的步长(如1000个、10000个等)。然后,系统中的各个节点(如微服务、应用服务器等)从该中心服务申清一个号段。一旦一个节点获取了一个号段,它就可以在这个号段内生成多个ID,而不需要再次与中心服务交互。 号段的范围是预先确定好的,例如:节点A获取到的号段是[1001,2000],节点B获取到的号段是〔2001, 3000],每个节点内部的ID是按顺序递增的。

3pc提交分布式事务

上面的2PC中介绍过了两阶段提交的原理和他主要存在的问题,在 2PC 中,如果协调者(Coordinator)在关键 节点挂掉,参与者 (Participant)可能会一直阻塞,甚至出现 数据不一致的情况。于是 3PC 在 2PC 的基础上加 入了一个額外的阶段(预提交阶段),并引入了 超时机制,让参与者在没有收到协调者指令时也能自己做决定,从而减少阻塞问题。3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit 阶段(询问阶段)协调者向所有参与者发送CanCommit 请求,询问是否可以执行事务。参与者: 如果可以执行(比如本地检查通过),返回 Yes:如果不可以执行,返回 No。这一阶段相当于 2PC 的第一阶段,但只是“问一下”。

PreCommit 阶段(预提交阶段)如果所有参与者都返回 Yes:协调者向所有参与者发送 PreCommit 请求,进入预提交阶段。参与者执行事务操作(写日志、加锁),井进入 等待提交状态。此时参与者可以安全地提交事务,但还没有最终提交。如果有任何參与者返回 No:协调者发送 Abort 请求,所有参与者回滚事务。

这一阶段是 3PC与 2PC 的关键区别:参与者已经进入一个“可以提交”的安全状态。 DoCommit 阶段(提交阶段)

如果 PreCommit 阶段所有参与者都确认成功:协调者向所有参与者发送 DoCommit 请求。参与者正式提交事务并释放资源。如果有失败或超时:协调者发送 Abort 请求,参与者回滚事务。如果协调者在这个阶段挂掉,参与者可以依赖超时机制自行提交事务,从而避免阻塞。

什么是雪花算法的时钟回拨问题

所谓时钟回拔,是指生成分布式ID的服务器上的系统时间,由于某种原因,突然相比之前的时间发生倒退了。产生 始终回拨的主要原因通常包括: 1.网络时间协议(NTP)同步:这是最常见的原因。操作系统会自动与NTP服务器同步时间,如果本地时间快 了,NTP会强制将时间回调。 2.人工手动修改:运维人员误操作,手动将系统时间改错了。 3.润秒调整:极少数情况下,操作系统处理闺秒时可能采用“倒退一秒”的策路。

而当前时间的时间戳作为雪花算法中重要的一部分,一旦时间戳倒退了,那么就意味着单台机器上生成的ID可能会 出现不顺序递增的情况,当然,更加严重的就是发生重复。

解决时钟问题,有几种典型的方案: 1、等待法

如果回拨时间非常短(比如几百毫秒左右),我们可以选择等待一小段时间,让系统时间追上来。实现方式就是让 当前线程sleep一下,然后再执行。

2、动态切换机器标识或者数据中心标识

如果发生时钟回拨,这时候我们可以通过动态切换一下机器标识或者数据中心标识等方式,来通免生成的ID重复。 因为机器ID有5个bit,那么我们就可以提前预留一个bit,当出现回拨时,就把这个一个bit运用上,这样机器ID变 了,即使其他的都相同,那么整个生成的结果也就不一样了。

3、直接地异常 4、提前生成一批ID备用

在上面的方案的基础上,如果时钟回拨的时间比较长的话,也可以考虑提升生成一批ID,存在Redis里而,一旦出 现时钟回拨了,那么就可以去Redis中取出之前生成好的ID来用。

什么是一致性hash

一致性哈希(Consistent Hashing)是一种用于分布式系统中数据分片和负载均衡的算法。它的目标是在节点的 动态增加或删除时,尽可能地减少数据迁移和重新分布的成本。 实现一致性哈希算法首先需要构造一个哈希环,然后把他划分为固定数量的虚拟节点,如2^32。那么他的节点編 自号就是 0-2^32-1:

接下来,假设有128张表作为节点映射到这些虚拟节点上,每个节点在哈希空间上都有一个对应的虚拟节点:

在把这些表做好hash映射之后,我们就需要存储数据了,现在我们要把一些需要分表的数据也根据同样的算法进 行hash,并且也将其映射哈希环上。

这样,这个hash环上的虚拟节点就包含两部分数据的映射了,一部分是存储数据的分表的映射,一部分是真实要 存储的数据的映射。 那么,我们最终还是要把这些数据存储到数据库分表中,那么做好哈希之后,这些数据又要保存在哪个数据库表 节点中呢? 其实很简单,只需要按照数据的位置,沿着顺时针方向寻找,找到的第一个分表节点就是数据应该存放的节点:

因为要存储的数据,以及存储这些数据的数据库分表,hash后的值都是固定的,所以在数据库数量不变的情况 下,下次想要查询数据的时候,只需要按照同样的算法计算一次就能找到对应的分表了。

我们首先要将新增加的表通过一致性hash算法映射到哈希环的虚拟节点中:

这样,会有一部分数据,因为节点数量发生变化,那么他顺时针遇到的第一个分表可能就变了。

相比于普通hash算法,在增加服务器之后,影响的范围非常小,只影响到一部分数据,其他的数据是不需要调整 的。

所以,再总结一下。一致性哈希算法将整个哈希空间视为一个环状结构,将节点和数据都映射到这个环上。每个节 点通过计算一个哈希值,将节点映射到环上的一个位置。而数据也通过计算一个哈希值,将数据映射到环上的一个 位置。 当有新的数据需要存储时,首先计算数据的哈希值,然后顺时针或逆时针在环上找到最近的节点,将数据存储在这 个节点上。当需要查找数据时,同样计算数据的哈希值,然后顾时针或逆时针在环上找到最近的节点,从该节点获 取数据。

会出现数据倾斜。/节点不能删除太频繁。

实现分布式锁-

互斥,最基本的条件,避免死锁/阻塞非阻塞,可重入/可靠性/锁的性能

不建议使用数据库唯一性做幂等

依赖异常,用异常进行流程控制,随着版本迭代异常变更,就有可能导致发生错误,依赖数据库,对数据库压大

分库分表

什么是分库分表

分库主要解决的是并发量大的问题。比较典型的分库的场景就是我们在做微服务拆分的时候,就会按照业务边界, 把各个业务的数据从一个单一的数据库中拆分开,分别把订单、物流、商品、会员等数据,分别放到单独的数据库 中。

分表主要解决的是数据量大的问题。通过将数据拆分到多张表中,来减少单表的数据量,从而提升查询速度。

分表字段如何选择?

在分库分表的过程中,我们需要有一个字段用来进行分表,比如按照用户分表、按照时间分表、按照地区分表。这 里面的用户、时间、地区就是所谓的分表字段。 那么,在选择这个分表字段的时候,一定要注意,要根据实际的业务情况来做慎重的选择。 通常,如果有特殊的诉求,比如按照月度汇总、地区汇总等以外,我们通常建议大家按照买家Id进行分表。因为这 样可以避免一个关键的问题那就是——数据倾斜(热点数据)。

水平拆表和垂直拆表

垂直拆表?将一个宽表拆分成多个表,每个表包含原表的一部分列。

  1. IO性能优化
    • 数据库按页读取(如4KB/页),列越多,单页存储行数越少
    • 拆分后,核心查询只需加载更小的数据页
  2. 热点数据分离
    • 基本信息和详细资料访问频率不同
    • 核心表可以放入更快存储
  3. 安全权限控制
    • 敏感列(如工资、身份证号)可以放在不同表,单独授权

按行拆分,将表的数据按某种规则分布到多个结构相同的表中。

范围分片、哈希分片、地域分片

  1. 数据量过大
    • 单表数据超过千万级别,查询性能急剧下降
    • 拆分后每个表数据量可控
  2. 写入瓶颈
    • 高并发写入场景,单表成为瓶颈
    • 分散到多个表提升写入吞吐量
  3. 热数据分离
    • 新数据频繁访问,历史数据很少访问
    • 可以分开存储和备份

AI

ai中rag的处理数据哪几种方案

固定大小的切分,根据预定的字符数字,单词数/或token数量。可能会破坏语义流畅性,建议在连续段落间保留一些重叠。实现简单,有助于批处理。

语义切分。根据句子,段落或者主题部分等有意义的单元来切分文档,