导图社区 Java 高频面试题
这里集合了绝大部分的高频面试题及其答案,欢迎各位批评指正,有需要的朋友赶紧收藏吧!
编辑于2024-03-22 13:26:46Java 八股高频面试题
计算机网络部分
三次握手的步骤是怎样的
先说三次握手的步骤
首先第一次握手,客户端向服务端发起建立连接请求,发送标志位 SYN=1 到服务端,客户端状态由 CLOSE 变为了 SYN-SENT,服务端依旧是 LISTEN。 第二次握手是服务端接受到了客户端的报文,返回 SYN + ACK 确认,服务端状态由 LISTEN 变为了SYN-RCVD,客户端还是 SYN-SENT。 第三次握手是客户端接受到服务端的报文后再想服务端发送报文 ACK 确认,第三次握手后客户端和服务端的状态都为 ESTABLISHED,此时连接建立完成。
三次握手图解如下
为什么不是两次握手
第三次握手主要为了防止已失效的或说阻塞的连接请求报文段突然又传输到了服务端,失效后重发数据包而服务端处理了两次同样的包到的连接,客户端认为是一次连接,而服务端认为是两次连接的状态不一致。三次握手可以总结为 "一次发送,两次确认" 这第二次确认就避免了状态不一致。 如果不采用三次握手,只要 B 发出确认,就建立新的连接了,此时 A 不会响应 B 的确认且不发送数据,则 B 一直等待A发送数据,浪费资源。
请简述 HTTP 常见状态码有哪些
状态码家族
首先 1 开头。
是信息性状态码。
2 开头。
表示成功。
3 开头的
是重定向。
4 开头的
是客户端错误。
以 5 开头的
是服务器错误。
常见状态码
状态码 200。
即成功。
状态码 301。
即永久性重定向。
状态码 302。
即临时性重定向。
状态码 400。
即错误响应。
状态码 403。
即被禁止的。
状态码 404。
即未找到。
状态码 500。
即网络服务器错误。
状态码 503。
服务器不可用。
什么是 JWT 以及其流程
JWT 是什么
JSON Web Token ( JSON 形式网络令牌 ) ,所以本质是 JSON 字符串,是用来校验请求接收后是否被篡改。
JWT 的组成
包含三个部分,标头、有效载荷和签名。 标头存储 JWT 元数据的 JSON 对象,包含了算法和类型两个字段,算法默认为 HS256,类型是令牌类型统一为 JWT。 有效载荷是存储的需要传递的数据的 JSON 对象,包含了发行人、到期时间、主题、用户等七个要素。 签名是对标头和有效载荷的数据签名,签名过程是使用 base64 编码后的 header 和 payload 数据,通过标头指定的算法默认是 HS256 算法生成哈希,以确保数据不会被篡改。
JWT 的特点
轻量级,JWT 很紧凑又包含必要信息。所谓的自包含,无需再服务端再查找用户信息,直接包含在 JWT 之中,因此就很是何无状态应用,可以反复利用 JWT 中的数据。可验证、可加密、可续签。它是很通用的,跨域、跨语言。传输效率高,Base64 编码,无需序列化和分序列化。
说说 JWT 工作流程
首先,用户认证,用户通过前端发送包含用户名和密码的 POST 请求到后端服务,通常使用 HTTPS 保证传输安全。然后,后端生成 JWT,后端验证用户凭据成功后,创建一个JWT。再就是,后端返回 JWT,后端将生成的JWT作为响应返回给前端。再存储 JWT,前端收到 JWT 后,将其存储在本地存储 ( 如 localStorage ) 或 cookie 中。前端存储后再每次都携带 JWT 发送请求,将 JWT 放在 HTTP 请求头的 Authorization 字段中发送给后端。后端对 JWT 验证。最后后端再处理请求,验证通过后,后端根据用户信息处理请求并返回相应结果。
什么是 IO 多路复用
单个线程同时操作多个客户端请求。前支持I/O多路复用的系统调用有 select、pselect、poll、epoll 四种调用方式。 首先说 select 调用:查询有多少个文件描述符需要进行 IO操作,特点:轮询次数多,内存开销大,支持文件描述符的个数有限。 再说 poll 调用:和 select 差不多。但是它底层数据结构是链表,所以支持文件描述符的个数没有上限。 最后说 epoll 调用:它就是更加高效的调用方式了,底层的数据结构是红黑树加链表,避免大内存分配和轮询。
TCP 如何保持可靠性传输的
有这么几个方式吧,数据包检验机制、失序数据包重排序、丢弃掉重复的数据、连接应答机制 ( 体现在 ACK 报文嘛 ),超时重发机制,流量控制 ( TCP 连接的每一方都有固定大小的缓冲空间 )。
四次挥手的步骤是怎样的
首先第一次挥手是 A 发送 FIN,A 向 B 发送一个 FIN 报文段 ( 表示 A 没有数据要发送了 ),A 进入 FIN-WAIT-1 状态。 第二次挥手就是 B 确认,B 收到 FIN 后,发送 ACK 报文段作为确认,B 进入 CLOSE-WAIT 状态,A 收到 ACK 后进入 FIN-WAIT-2 状态。 第三次就是 B 发送 FIN,B 发送完所有数据后,向 A 发送一个 FIN 报文段,B 进入 LAST-ACK 状态。 第四次 A 确认并等待,A 收到 FIN 后,发送 ACK 报文段作为确认,然后进入 TIME-WAIT 状态等待 2MSL 时间后,彻底关闭连接。B 收到 ACK 后,立即关闭连接。
请简述 TCP 和 UDP
TCP 是可靠传输,UDP 是不可靠传输。 TCP 面向连接,UDP 无连接,即发送数据之前不需要建立连接。 TCP 传输数据有序,UDP 不保证数据的有序性。 TCP 面向字节流,把数据看成一连串字节流,UDP 是面向报文的。 TCP 传输速度相对 UDP 较慢。 TCP 有流量控制和拥塞控制,UDP 没有。 TCP 是重量级协议,UDP 是轻量级协议。 TCP 首部20字节,UDP 首部8字节。 TCP 连接只能是一对一的 ( 端到端 ); UDP 支持一对一、一对多、多对一和多对多的通信方式。
请简述 HTTP 和 HTTPS 区别
一个免费一个收费,HTTP 是免费的,而 HTTPS 需要到 CA 机构申请证书,还需要缴纳费用。 一个明文一个密文,HTTP 是超文本传输协议,信息是明文传输的,HTTPS 则是具有安全性的 SSL/TLS 加密传输协议,信息是密文。 一个无状态一个安全,HTTP 的连接很简单,无状态;HTTPS 协议是由 SSL/TLS+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。 占用内存资源不一样,和 HTTP 通信相比,HTTPS 通信会由于加减密处理消耗更多的 CPU 和内存资源。 一个端口是 8 0 一个端口是 4 4 3,HTTP 端口是 8 0 和 HTTPS 端口是 4 4 3。
简述 HTTP 请求转发和崇定向的区别
它们的主要区别
请求和响应次数不一样,请求转发是一次请求,收到一次响应,重定向是发两次请求,收到两次响应。 传输的数据可读性不同,可以直接获取到请求转发中所携带的数据,而重定向的资源不能直接获取。 浏览器显示的地址不同,请求转发是显示的用户所提交的请求路径,且不会改变,显示的为重定向的请求路径,而非用户提交请求的路径。这就很好的防止恶意的表单重复提交。 跳转的范围不同,请求转发只能跳转到当前应用的资源中,而重定向还可跳转到其它应用中的资源。
如何选择请求转发还是崇定向
—般选转发。如果需要跳转到其它应用,肯定重定向。如果处理表单数据的 servlet 需要跳转到其它的 servlet,则需要选择重定向,防止表单重复提交。( 比如说 登陆接口 )。
Java SE 部分
请简述一下 JDK、JRE 和 JVM
首先,JDK 是 Java 开发包,它包括了 JRE 和 Java 的开发工具 ( 如编译工具 javac.exe 、打包工具 j a r.exe、等 Java 基础的原生工具 )。 JRE 即 Java 运行时环境,它呢包括 Java 核心类库 和 JVM。 最后,JVM 就是 Java 虚拟机,不同的平台有自己的虚拟机,Java 语言借助它实现了 跨平台 功能。 三者结构层次是,JDK 中包含了各种 .exe 工具和 JRE ,JRE 中又包含了 Java 核心类库 和 JVM。
请简述一下 == 与 equals 的区别
先说 == 运算符,== 是比较运算符,如果比较的是基本类型,比较存储的值;如果比较的是引用类型,比较的是所指向对象的地址值是否相等 ( 即是否是同一个对象 ),它可以直接和 null 比较。 再说 equals 方法,是 Object 这个超类的的方法,所以 Java 原生类可以选择重写它,同时它因此无法比较基本类型,比较引用类型时,默认非崇写的 equals 是用来比较两个对象的地址值是否相等,因为它的底层就是this对象也就是调用该方法的对象和参数对象作==比较,用 equals 方法判断前应该判断该变量是否 null。
请简述一下 hashCode 与 equals
请简述一下 equals 的底层
Object 中的 equals 方法源码,直接就是返回 this 即调用对象与参数对象 == 比较的结果。 String 中的 equals 方法源码,先判断调用方法的字符串对象是不是和参数对象是同一个对象再判断参数的对象是否是字符串对象,如果是,依次比较字符数组的数组。
请简述一下 hashCode 的底层
hashCode 的底层源码,很简单,就只有一行 native 方法,封装在 JVM 底层,默认即非崇写的情况是根据对象的内存地址值计算 hashCode 的。 hashCode 的常见崇写形式,先用 点 hash 方法对对象的一些关键属性计算哈希码再对哈希码进行取模。
为什么崇写 equals 方法必须也要崇写 hashCode 方法
保证 equals 和 hashCode 的一致性,equals 判断为相等那 hashCode 也必须判断为相等,不重写的话可能在一些情况下认为是相等的对象是不相等的。 对象在存入 HashMap 和 HashSet 等集合时是根据相同的哈希码将其存入同一个桶 ( bucket ) 中的,否则会导致存入和检索出错。 用 hashCode 取代 equals 判断对象相等可以提高性能,虽然会引入哈希冲突的情况,但是可以有兜底措施即用 equals 再比较一遍,equals 为 true 相同 HashMap 是直接覆盖,而 HashSet 是选择不添加该新元素,equals 为 false 则散列到树的其他位置存储,或是集合数组中的一个元素即一个链表的末尾。
请简述一下面向对象的五大基本原则
单一责任原则,每个类应该有且仅有一个引起它变化的原因,换句话说,一个类应该只负责一项职责,这有助于降低类的复杂度,使得类更容易理解和维护。 开放-封闭原则,软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改现有代码的情况下,应该能够通过扩展来添加新的功能或行为。 里氏替换原则,子类必须能够替换其父类,而程序仍然可以保持正确的行为, 换句话说,派生类应该能够在不破坏程序正确性的前提下替代基类。 依赖倒置原则,高层模块不应该依赖于低层模块,二者都应该依赖于抽象,同时,抽象不应该依赖于具体实现,具体实现应该依赖于抽象,这可以通过使用接口或抽象类来实现。 接口分离原则,不应该强迫客户端实现它们不需要的接口,应该将大的接口 拆分为更小的、更具体的接口,以便客户端只需实现所需的接口。
请对比一下 接口 和 抽象类
二者共同点
都不能被实例化,只能被继承或实现。
都可以包含抽象方法,需要子类或实现类去全部实现。
都可以包含具体方法。
都可用来实现多态。
二者不同点
一个类只能有一个抽象类,但是可以有多个接口。
抽象类可以有构造方法,但是接口不能有。
抽象类的子类可以选择性崇写抽象方法,但是接口的实现类必须实现所有抽象方法。
抽象类的访问修饰符可以是 public、protected 和 默认, 但接口的必须是 public 或 默认。
抽象类的抽象方法访问修饰符可以是 public、protected 和 默认, 但接口的抽象方法必须是 public。
你熟悉哪些常见的设计模式
单例模式
首先单例模式的设计意图是保证一个类仅有一个实例,并提供且仅提供一个访问它的全局访问点。 单例模式的设计的结构是这样的一个类只产生一个实例,在该类中定义一个本类实例的静态属性 instance,再用一个公有的静态 Get 方法 getInstance() 对外返回这个属性,在使用时 ( 创建这个类的对象时 ) 等号右边不用 new,而是用该类点.getInstance() 单例模式的使用场景有Spring 的 Bean、Servlet 容器 ( 如 Tomcat 的 Servlet )、常见的单例的数据库连接池的使用场景。
工厂模式
工厂方法模式相较于简单工厂模式呢,它让一个类的实例化延迟到其子类 ( 这里则是 Factory 接口的实现子类 FactoryA 或 B ),创建"工厂"接口,让其实现子类决定实例化哪个接口,它让一个类的实例化延迟到了子类。 工厂方法模式它的设计的结构是这样的,有一个"产品"这个名词概念本身 ( Product ) ,再有产品A/B的设计概念 ( ProductA/B ) 有一个"工厂"这个概念/名词本身 ( Factory ) 以及生产产品的"车间"的名词概念,然后有这两个工厂设计概念 ( FactoryA/B ),都有负责生产产品的"车间" ( createProduct()方法 ) 依赖产品A/B的设计概念 ( ProductA/B ) 图纸;然后用真正建成的工厂A/B ( factoryA/B对象 ) 的车间 ( factoryA/B.createProduct(); ) 去制造产品A/B ( productA/B对象 )。 工厂方法模式使用场景有 Spring 的 BeanFactory 还有 MyBatis 中 SqlSessionFctory。
抽象工厂模式它打破了工厂方法模式的一个工厂只生产一种产品的限制。它提供了一个创建一系列相关或相互依赖对象的接口,而无须指定他们具体的类。 抽象工厂模式它的结构是这样的,抽象工厂模式涉及多个产品族的创建,每个产品族由多个产品组成。我们有一个"产品"这个名词概念 (Product),产品A/B属于不同的产品族 (ProductFamilyA/B)。同时,有一个"工厂"这个概念/名词本身 (Factory),以及生产产品的"车间"的名词概念。然后有这两个工厂设计概念 (AbstractFactoryA/B),它们都有负责生产产品的"车间" (createProductA()/createProductB()方法)。这些"车间"依赖产品A/B的设计概念 (ProductA/B) 图纸。接着,通过真正建成的工厂A/B (factoryA/B对象) 的车间 (factoryA/B.createProductA(); factoryA/B.createProductB();) 去制造产品A/B (productA对象, productB对象)。
代理模式
代理模式它为其他对象提供一种代理以控制对这个对象的访问。 代理模式的结构是这样的,代理模式涉及通过代理对象来控制对另一个对象的访问。有一个"主体"这个名词概念 ( Subject ),以及它的具体实现类 ( RealSubject )。同时,有一个"代理"这个概念/名词本身 ( Proxy ),它充当了主体的代表,提供了与主体相同的接口,以便在需要时代理实际的主体。然后有一个代理设计概念 ( Proxy ),它实现了与主体相同的接口,并在必要时调用真正的主体。代理对象依赖主题对象,以便完成它的职责。接着,通过创建一个代理对象 ( Proxy 对象 ) 并使用它来控制对主体对象 ( RealSubject 对象 ) 的访问。 代理模式的使用场景有 Spring 的 AOP 动态代理,而 AOP 要么是要么是 JDK 代理,要么是 CGLIB 代理,还有 MyBatis 的 Mapper 代理。
模板方法模式
模板方法模式的设计意图就是定义一个操作中的算法骨架,而将一些步骤延迟到子类中,Template Method 使得子类可以不改变一个算法的结构即可崇定义该算法的某些特定步骤。 子类实现父类方法还是自由的,但是定义了一套模板其中规定死了如何调用方法。 模板方法模式的使用场景有这些,Java 原生的 Servlet 还有 Spring 的 JdbcTemplate。
策略模式
策略模式它定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换,此模式使得算法可以独立于使用它们的客户而变化。 策略模式的结构是这样的,主旨是将一个工作分为几种策略完成,有一个 "策略" 这个概念 ( Strategy ),再有不同策略的设计的概念 ( StrategyA/B )。有一个 "执行者" 这个概念本身 ( Executor ),它负责执行具体的策略。然后有这两个策略的设计理念 ( StrategyA/B ),设计理念中有负责执行策略的方法 ( execute() 方法 )。接着,通过执行者类创建的 executor 对象 调用执行策略的方法 ( executor 点 execute(); ) 来执行不同的策略。这样,根据需要可以动态地改变策略,而不需要修改执行者的代码。 策略模式的使用场景有这些,Spring 的 AOP 中不同的通知类型就是不同的策略,JDK 的 Comparator ( 比较器 ),可以动态地选择不同的比较策略,例如升序、降序等。JDK 的 Collections 的 sort() 方法,JDK 的 Executor ( 线程池 ),Spring Security 的 认证和授权策略。
装饰器模式
装饰器模式的设计意图就是动态地给一个对象添加额外的职责。 现有有一个"组件"这个概念 ( Component ),再有装饰器的设计理念 ( Decorator )。装饰器是一种特殊的组件,它可以动态地扩展另一个组件的功能。有一个"装饰器工厂"这个概念 ( DecoratorFactory ),它负责创建装饰器实例对象。然后有这两个装饰器的设计理念( DecoratorA/B ),都有负责扩展组件功能的 ( operate() 方法 ) 。接着,通过装饰器工厂类 ( DecoratorFactory 类 ) 创建装饰器 ( Decorator ) 对象,并将装饰器对象应用到组件对象上,也就是用装饰器对象调用 operate() 的方法。这样,可以动态地添加或修改组件的功能,而无需改变组件本身的代码。 看起来具体组件类是多余的,但是如果没有具体组件类,那么所有的装饰器都需要从头开始实现一个基本的被装饰对象,这会让装饰器模式变得更加繁琐、复杂。 装饰器模式的使用场景有例如 Spring 的 AOP 切面 ( Aspect ) 可以被看作是一种装饰器,它可以在原有的方法执行前后添加额外的逻辑,而不需要改变原有方法的实现。
享元模式
享元模式的设计意图就是运用共享技术有效地支持大量细粒度 ( 细粒度指的是对象的粒度越小,所代表的实体就越具体、具体性越强,可以表示更细致的特征和状态 ) 的对象。 有一个"棋子"这个概念 ( Piece ),再有黑子和白子的设计理念 ( BlackPiece/WhitePiece )。享元模式是一种结构型设计模式,它通过共享对象来减少内存使用和提高性能。在享元模式中,有一个"棋子工厂"这个概念 ( PieceFactory ),它负责创建并管理棋子对象。然后有这两种棋子的设计理念 ( BlackPiece/WhitePiece ) 类,它们都有自己的颜色和形状属性。 接着,通过棋子工厂创建棋子对象 ( blackPiece 对象/whitePiece 对象 ),并在需要时共享已经创建的棋子对象。这样,可以减少内存消耗并提高性能,因为多个相同颜色和形状的棋子可以共享同一个对象。 享元模式的使用场景有各种池,字符串常量池、数据库连接池和线程池。
什么是 BIO、NIO、AIO
首先说 BIO,blocking IO,即 阻塞式IO,也称为 同步IO,阻塞是指读取无数据则一直等待,这就导致了大量连接时性能受影响,长时间等待、浪费线程资源。 再说 NIO,Non-blocking IO,即 非阻塞式IO,也称为 同步非阻塞IO,非阻塞是指轮询检查无需等待,一个线程可以管理多个连接,通过轮询的方式检查是否有数据可用,不需要等待数据到达。引入通道与缓冲区的概念,提供了更灵活和高效的 IO 操作方式。 最后说 AIO,Asynchronous IO,即 异步IO,也称为非阻塞异步 IO,应用程序发起 IO操作后不会被阻塞,而是继续执行其他任务,当 IO操作完成时,会通过回调通知应用程序。
"abc" 和 new String("abc") 有什么区别
共同点,并不意味着前者是变量,而是他们都是对象。 不同点,不是同一个对象,前者 即 双引号 "abc" 是直接存在常量池,而 new 出来的,直接在堆区创建 String 对象 ( 对象的内存地址 ) ,在执行代码时,会在栈内存中创建一个变量,该变量引用堆内存中的字符串对象。 注意,如果是 String str1 = "abc" 和 String str2 = "abc" 是同一个对象。
字符串的比较
用 == 比较字符串,是比较的是是否为同一个 String 对象,即引用是否相同。 equals 比较字符串,比较的是字符串内容是否相同。 崇写 equals 达到高性能比较,先比较长度,长度不同直接 false,再比较 HashCode,不同直接 false,有可能 Hash 冲突,字符串不一样 HashCode 一样,这样就再逐一比较字符数组的元素。
字符串的不可变性
字符串的不可变性是什么
是指,操作 String 对象时对其的修改不会改变底层对应的字符串的值,而是在底层 JVM 创建多个你每次修改过后的字符串。
字符串的不可变如何实现
final 修饰 String 的核心变量 char value[];
String 类的成员变量 ( 其本质的 char 数据 ) 被声明为 final, 这意味着一旦初始化,它们就不能被修改。
保护性拷贝机制
在想要对原有字符串内容进行更改时,不会对 char value[] 修改, 而是会将创建新对象然后接受更改后的数据。
字符串的不可变性的优点
1. 线程安全
由于字符串不可变,它们在多线程环境中更加安全,多个线程可以同时访问相同的字符串对象,而不需要担心数据修改的同步问题。
2. 数据安全性
字符串不可变性保证了字符串的内容不会被意外修改, 从而避免了一些潜在的错误和安全问题。
3. 复用、缓存优化
由于字符串不可变,可以在适当的情况下对字符串进行缓存,以提高性能因为相同内容的字符串在内存中只会有一个实例,可以节省内存空间。
说说你常用的 String 方法
split 方法
按特定字符作为分隔符将字符串拆分。
substring 方法
获取字串。
trim 方法
去除字符串首尾的空白字符。
toUpperCase 方法
将字符串转换为大写。
toLowerCase 方法
将字符串转换为小写。
当然必须要有 equals 方法
比较字符串是否相等。
请对比一下 String、StringBuffer 和 StringBuilder
三者的可继承性
只有 String 类被 final 修饰无法被继承。StringBuffer 和 StringBuilder 均可被继承。
三者的可变性
String 类是不可变的,StringBuffer 和 StringBuilder 是可变的,String 一旦创建,它的值不能被修改,任何对字符串的操作都会返回一个新的字符串对象,保护性拷贝。StringBuffer 和 StringBuilder 它们允许在原始字符串 ( 字符数组 ) 上进行添加、插入、删除和修改等操作。
三者的线程安全性
String 和 StringBuffer 是线程安全的,StringBuilder 不是线程安全的,因为 String 是不可变的,线程安全,而 StringBuffer 的方法都被 synchronized 关键字修饰,因此线程安全,但性能相对较低。再说到 StringBuilder ,它的方法没有同步措施,因此在单线程环境中性能更好,但不适合多线程环境。
三者的性能角度
String 操作性能较差,StringBuffer 和 StringBuilder 操作性能较好,但是多线程下 StringBuilder 更好,String 因为每次修改都会创建一个新的字符串对象。而 StringBuffer 和 StringBuilder 特别是当需要进行大量字符串拼接或修改操作时性能更好,StringBuilder 的性能最佳,因为它没有额外的同步开销。
三者的底层区别
String 底层,基于字符数组 ( char[] ) ,它的不可变性是通过 private final char[] 属性和不提供修改方法来实现的。 StringBuffer 和 StringBuilder 底层,也基于字符数组,但它们提供了可变的操作方法,通过扩容字符数组或在原始数组上进行操作来实现字符串的修改,区别在于 StringBuffer 的方法都使用了 synchronized 同步关键字,以保证线程安全,而 StringBuilder 没有同步。
三者的适用性
String,需要处理不可变的字符串的情况使用。 StringBuffer,在多线程环境下需要修改字符串的情况使用。 StringBuilder,在单线程环境下需要高性能的字符串操作的情况使用。
请说说你熟悉的集合
ArrayList
ArrayList 是数组的升级版,ArrayList 它有一些关键属性属性,核心数据属性 Object 数组 elementData,存放长度状态的属性 int 类型的 size 属性,默认长度的常量,值为 10。 ArrayList 它的特点有 ArrayList 自动扩容机制和 ArrayList 主动缩容,先说说自动扩容机制吧,首先初始化。后续在添加第一个元素时,当第一个元素被添加到 ArrayList 中时,它会创建一个初始容量为 10 的数组 ( 默认的初始容量 ),size 变量会置为 1 ,即长度为 1 ,下一次添加的下标也是 1 即 size。扩容时,扩容就是数组的浅拷贝,将旧数组中的数据拷贝到新数组里。当存满但不超过上次扩容的长度的 1.5 倍时扩容至原本长度的 1.5 倍。当存完之后,总长度超过原长度 1.5 倍,会触发多次扩容,直至扩容到容纳下为止。主动缩容机制,ArrayList 支持缩容,但不会自动缩容,需要自己调用它的 trimToSize() 方法,此时数组将按照元素的实际个数进行缩减。
LinkedList
LinkedList 它统一了双向链表 ( 和普通链表 ) 、双端队列 ( 和先进先出队列 ) 、堆栈。它有这些核心成员一个静态内部类 Node 代表节点,该内部类的成员 item 保存了当前元素,成员 next 保存下一个元素,成员 prev 保存上一个元素。一个头指针 ( 核心数据 ) Node 类型 名为 first。一个尾指针 ( 核心数据 ) Node 类型 名为 last。 LinkedList 它的特点有,实现了 Deque ( 而 Deque 实现了 Queue ) ,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。基于 双向循环链表,头节点为空,支持向前向后遍历,支持循环。它内部还实现了一个专门的迭代器。
HashMap
HashMap 集合是基于 Hash 散列表 + 红黑树 ( 也就是 数组 + 链表 或 红黑树 ) 的集合。 核心数据属性,Node 节点类型的名为 table 的属性,通常其中每个元素被人们称为 "桶" ,是一个存放键值对的容器,所以 "桶" 只有可能存放三种情况 : 1.一个键值对,2.一个链表,3.一个红黑树。 核心操作 get、put 方法中具有根据哈希码计算哈希值的操作,根据 键 key 的哈希码本身与其哈希码的高 16 位异或运算得到哈希值。 HashMap 插入元素的流程,根据元素的 哈希值 和 数组的长度 计算出应存入 的位置,判断当前位置是否为 null,若是 null 直接存入,完成插入,如果位置不为 null,表示有元素,则调用 equals 方法比较属性值,equals 比较为 true 的话覆盖旧元素;不一样的话,如果当前位置是一个链表,新元素将被添加到链表的末尾,这意味着同一个链表中具有相同的哈希码但哈希值不同。如果当前位置是一个红黑树,新元素将根据其哈希值和键的比较结果被插入到树中的适当位置。 HashMap 的扩容机制,首先 HashMap 的默认长度为 16。负载因子 或 加载因子 又或者说 扩容因子在 ( 0, 1 ] 左开右闭区间的浮点数。扩容长度为 2 倍。重新 hash 机制。JDK 8 不会重新 hash,在 JDK 8,扩容时不需要崇新计算元素的哈希值,而是根据元素的哈希值的最高位来确定新的位置,提高扩容的效率,避免崇新计算哈希值和取模运算,如果重新 hash 可能会拆链,因为它会用 int hash 桶下标 ( 哈希值 ) 在扩容中发生变化,也没说成是取模运算。HashMap 的扩容条件是,当插入新元素后,元素数量大于容量 乘 负载因子,且必须要发生 Hash 冲突。HashMap 的扩容流程,默认长度是 16 ,当 已经有 12 个元素后再插入新元素 ( 元素数量大于容量 乘 负载因子 ) ,且必须要发生 Hash 冲突,就会将原有长度 ( 16 ) 扩容至 2 倍 ( 32 )。 HashMap 树化与退化,树化是指红黑树化,树化条件,数组达到最小树化阈值 ( ≥64 ) ,且链表的长度大于 8。如果数组没有达到最小树化阈值 ( ≥64 ) ,但链表长度有大于 8 的,这种情况不会树化,只会优先考虑数组扩容。树化的目的是为了提高查询。再说退化,退化条件之一是扩容时,如果红黑树拆分成的两棵树的节点数都小于等于 6,那么会退化成链表,并插入到新的数组中,退化条件之二是删除时,如果红黑树的根节点为空,或者根节点的左子树或右子树为空,或者根节点的左子树的左子树为空,那么会退化成链表。退化的目的是为了避免红黑树过于稀疏,影响性能。 HashMap 并发的死链情况,死链问题有两种情况,一是由于某些原因导致链表中的某个节点的 next 指针指向了自己或者循环指向了前面的节点,从而形成了一个死循环,导致无法遍历或者插入新的节点,二是由于某些原因,元素的 hash 值被错误的计算,元素被错误的放入到了其他位置,造成链表的断裂。可以这样解决死链问题,一是用 ConcurrentHashMap 代替 HashMap、还有就是利用Collections.synchronizedMap() 包装,再就是插入 HashMap 元素时,尽量不用可变对象作为 键。 HashMap 是这样解决哈希冲突的,在插入元素时会首先会根据键的 hashCode() 方法计算出哈希值,然后用这个哈希值和 HashMap 的当前容量进行模运算来确定元素在内部数组中的位置,遍历该桶下标的链表或红黑树对每个元素调用 equals 方法来比较当前要插入的键与链表或树中的键是否相等,如果 equals 相等,那么 HashMap 会用新元素替换掉旧元素的值 ( 键保持不变,值更新 ),如果 equals 不相等,HashMap 会将新元素添加到链表的末尾或红黑树的适当位置。
TreeMap
TreeMap 是基于红黑树的,红黑树是一个二叉查找树,即呈现为左小右大,红黑树不是高度上平衡的,因为平衡二叉树,一旦左右子树高度差超过 1 时,通过旋转保持平衡,导致频繁旋转,最终导致插入性能降低。具有红黑规则,每一个节点或是红色的,或者是黑色的,而根节点必须是黑色,如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,每个叶节点 ( Nil ) 是黑色的。如果某一个节点是红色,那么它的子节点必须是黑色 ( 不能出现两个红色节点相连的情况 )。对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。 它有一个静态成员内部类 Entry 作为核心数据 - 红黑树,该内部类下又 key 属性存储键值,value 属性存储对应数据值,left 属性存储左子树,right 属性存储右子树,parent 属性存储父节点,最后 布尔类型 的 color 属性存储节点颜色。 它的核心成员有,比较器 comparator。根节点 ( 核心数据 ) Entry 类型的 root。大小 ( 即树中的节点数 ) size。modCount 即 modify count 也就是树结构的修改次数。 TreeMap 特点就是可排序的,Javabean 类实现 Comparable 接口的 compareTo() 方法,你不需要显式地调用它,只需要实现就行,TreeSet / TreeMap 底层你在添加元素时会调用它。创建集合时,自定义一个实现了 Comparator 比较器的比较器类,实现其 compare() 方法,实例化该类对象,将其传递给 TreeSet 或 TreeMap 构造方法,该方式会覆盖 实现 Comparable 接口 的方式。
HashSet
HashSet 是基于 HashMap 的,核心数据 是 HashMap 类型的 名为 map 的属性,核心数据存储在一个 HashMap ( 数组 + 链表 + 红黑树 ) 中,只不过取 HashMap 的键作为核心数据,不需要值存储数据,值 只需要占位 / 标记就行 ( 通常是 null ) HashSet 它的核心方法 add 添加方法 ( null 占位 value,将 e 存入 key ) 中 就一行代码,也就是 返回map点put e 逗号 PRESENT == nulld 的结果。 HashSet 的特点,无序性、无索引和唯一 ( 可以去崇 )。
请说说 ArrayList 和 LinkedList 的区别
1. 底层数据结构对比
ArrayList 的底层
使用数组作为底层数据结构,支持随机访问。
LinkedList 的底层
使用链表 ( Node 内部类定义链表前趋后继等 ) 作为底层数据结构,插入和删除元素更高效,但随机访问性能较差。
2. 随机访问性能的对比
ArrayList
在随机访问元素时性能优越,因为可以通过索引直接访问元素。
LinkedList
随机访问性能相对较差,需要从头部或尾部开始遍历链表,直到达到目标位置。
3. 插入和删除性能的对比
ArrayList
在中间插入或删除元素时性能较差,因为需要移动后面的元素。
LinkedList
在插入和删除元素时性能更好,因为只需调整相邻节点的引用。
4. 空间效率的对比
ArrayList
在插入和删除元素时可能需要分配新的数组,浪费一些内存空间。
LinkedList
不需要预分配内存空间,可以动态增长,因此在某些情况下更节省内存。
5. 迭代性能的对比
ArrayList
在迭代元素时性能良好,因为可以通过索引高效地访问元素。
LinkedList
在迭代时性能相对较差,因为需要按顺序遍历链表节点。
6. 适用场景的对比
ArrayList
如果需要频繁进行随机访问和遍历元素,并且元素数量相对固定,更适合。
LinkedList
如果需要频繁进行插入和删除操作,特别是在中间位置, 或者元素数量不确定,更适合。
请简述一下 JVM 内存模型
JVM 运行时的数据区
运行时数据区包括 方法区 ( JDK1.7 叫永久代 / JDK 1.8 叫元空间 )、堆区、Java 栈 ( 虚拟机栈 )、程序计数器 ( PC 寄存器 ) 和本地方法栈。 先说方法区方法区的存储了类信息、类的字节码和运行时常量池,方法区在物理硬盘之中。方法区的特点有,是所有线程共享的、只有一块所以共享。 再说堆区,堆区存储着对象实例,比如数组数据存储在堆区 ( 当然数组引用存储在栈 ),还包含有字符串常量池。堆区的特点,也是有且仅有一块,因此也是共享的,堆中对象内存分配被加锁,new 对象开销大,垃圾回收机制主要针对堆区,堆被分为两块年轻代和老年代,刚创建的对象存放在年轻代,而老年代中存放生命周期长久的实例对象。 然后就是 Java 栈 ( 或者说虚拟机栈 ),每个线程都有自己的 Java 栈,可以说它是一个线程,因此不是共有的,栈中存放了所有对象的引用和栈帧,栈帧中又存储了方法的局部变量表、方法的操作数栈、方法的动态链接、方法返回地址等。 还有就是程序计数器 ( 或者说 PC 寄存器 ),它记录着当前线程所执行的字节码的行号指示器。程序计数器的特点有线程独立,每个线程私有一个程序计数器,程序计数器存储了一个值,该值为对应线程执行字节码指令的地址,并且它是唯一一个不会发生 O-O-M 即内存不足错误的数据区域。 最后就是本地方法栈,存放 Native 方法即本地方法的调用帧,本地方法的调用帧包括了本地方法的局部变量表、本地方法的操作数栈还有本地方法的返回地址。特点是线程不共享,且所有栈的大小都是可调整的。
三大变量的内存位置
静态变量 ( 静态属性 )
存放在方法区 ( 的静态变量区域 )中。
局部变量 ( 方法体中变量 )
局部变量的引用存放在栈中。
实例变量 ( 非静态属性 )
存放在堆中。
为什么打破以及如何打破双亲委派机制
什么是打破双亲委派机制
是指自定义类加载器不按照默认的方式去委托父类加载器加载类, 而是直接或者有选择地自己加载类
打破双亲委派机制的目的
实现类的隔离和共享,如 Tomcat 中每个 Web 应用程序都有自己的类加载器,可以加载不同版本的同名类库,而公共的类库则由父类加载器加载,实现了不同应用程序之间的隔离和共享。 实现类的热替换和动态更新,如 JSP 文件修改后不用崇启服务器,就可以崇新编译成 .class 文件并由新的类加载器加载,实现了 JSP 文件的 HotSwap 功能。 实现 SPI ( Service Provider Interface ) 机制,例如 JDBC 中通过线程上下文类加载器来加载不同厂商提供的驱动实现类。
如何实现打破双亲委派机制
打破双亲委派机制的方法是自定类加载器,继承 ClassLoader ,并崇写 ClassLoader 的 loadClass 方法 改变默认的委派逻辑。 如 : 可以直接调用 findClass 方法来尝试自己加载类,而不是先委托给父类加载器,或者可以根据类名或者其他条件来判断是否委托给父类加载器。
重要部分请再听一遍。
什么是打破双亲委派机制
是指自定义类加载器不按照默认的方式去委托父类加载器加载类, 而是直接或者有选择地自己加载类
打破双亲委派机制的目的
实现类的隔离和共享,如 Tomcat 中每个 Web 应用程序都有自己的类加载器,可以加载不同版本的同名类库,而公共的类库则由父类加载器加载,实现了不同应用程序之间的隔离和共享。 实现类的热替换和动态更新,如 JSP 文件修改后不用崇启服务器,就可以崇新编译成 .class 文件并由新的类加载器加载,实现了 JSP 文件的 HotSwap 功能。 实现 SPI ( Service Provider Interface ) 机制,例如 JDBC 中通过线程上下文类加载器来加载不同厂商提供的驱动实现类。
如何实现打破双亲委派机制
打破双亲委派机制的方法是自定类加载器,继承 ClassLoader ,并崇写 ClassLoader 的 loadClass 方法 改变默认的委派逻辑。 如 : 可以直接调用 findClass 方法来尝试自己加载类,而不是先委托给父类加载器,或者可以根据类名或者其他条件来判断是否委托给父类加载器。
请简述一下 JVM 垃圾回收 ( GC )
垃圾回收机制是将内存中不再被使用的对象进行回收,重点针对动态数据的回收,程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭, 因为方法结束或者线程结束时,内存自然就跟随着回收了,而 Java 堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所关注的部分。 有这些垃圾回收机制,新生代收集、老年代收集、混合收集和整堆收集。 新生代 GC,有复制算法和 Mark-Copy ( 标记-复制 ) 算法,内存划分:内存被分为两个相等的区域,一个是活动区域,用于当前对象的分配;另一个是空闲区域,初始时未被使用。标记阶段:当活动区域满时,垃圾回收器启动。它扫描活动区域,标记所有存活的对象。复制阶段:垃圾回收器将所有标记为存活的对象复制到空闲区域,并按顺序排列。更新引用:复制后,所有指向原对象的引用被更新为指向新复制的对象位置。清除和交换:未被复制的对象(即非存活对象)在活动区域被清除。在 Mark-Copy ( 标记-复制 ) 算法中,第一内存划分:内存被分为两个相等的区域,一个是活动区域,用于当前对象的分配;另一个是空闲区域,初始时未被使用。第二是标记阶段:当活动区域满时,垃圾回收器启动。它扫描活动区域,标记所有存活的对象。第三复制阶段:垃圾回收器将所有标记为存活的对象复制到空闲区域,并按顺序排列。第四更新引用:复制后,所有指向原对象的引用被更新为指向新复制的对象位置。最后清除和交换:未被复制的对象(即非存活对象)在活动区域被清除。之后,空闲区域变为新的活动区域,原活动区域变为空闲区域,准备下一次垃圾回收。 再说老年代 GC,Mark-Sweep ( 标记-清除 ) 算法,每个对象都会存储一个标记位,记录对象的状态 ( 活着或是死亡 )。标记-清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。优点是可以避免内存碎片。Mark- Compact ( 标记-整理 ) 算法,标记阶段:在标记阶段,垃圾回收器会遍历所有的存活对象,并将它们标记为活跃的。这一过程与标记-清除算法中的标记阶段类似,都是为了确定哪些对象是存活的,哪些是需要被回收的。整理阶段:在整理阶段,垃圾回收器会对存活的对象进行整理,将它们紧凑地排列在内存区域的一端,以消除内存中的碎片化。具体来说,垃圾回收器会将存活对象向内存区域的一端移动,同时保持它们之间的相对顺序不变。这样,所有的存活对象都会被连续地排列在一起,而空闲的内存空间则会集中在内存区域的另一端。
请说说你对异常的了解
try-catch-finally 捕获异常的形式
catch 和 finally 是可选的,但是省略 finally 是不推荐的。 finally 块很重要,有 finally 但并非一定执行,如虚拟机退出,如果程序中调用了System.exit()方法导致虚拟机退出,那么finally块不会执行,再如死循环,如果在try块中出现了死循环,导致无法正常退出,那么finally块也不会执行,最后例如线程死锁,如果在try块中出现了线程死锁,导致无法正常继续执行,那么finally块也不会执行。
什么是优先捕获最具体的异常
优先应该先处理子类异常,再处理父类异常,这样做的目的是为了避免父类异常捕获了本应由子类异常处理的情况,导致异常处理逻辑不正确或者丢失异常信息。
说说异常体系 ( 异常种类 )
Java 异常体系包括 Throwable ( 可抛出 ) 作为顶级类,它分为 Error ( 错误 ) 和 Exception ( 异常 ) 两个子类。Error ( 错误 ) 包括 VirtualMachineError ( 虚拟机错误 ) 和 ThreadDeath ( 线程终止 ) 等子类,其中 VirtualMachineError ( 虚拟机错误 ) 下包括了 OutOfMemoryError ( 内存溢出错误 ) 和 StackOverflowError ( 堆栈溢出错误 ) 等异常;ThreadDeath ( 线程终止 ) 则表示线程意外终止。Exception ( 异常 ) 是一般性异常类,包括 IOException ( 输入输出异常 ) 和 RuntimeException ( 运行时异常 ) 等。IOException ( 输入输出异常 ) 又包括了 FileNotFoundException ( 文件未找到异常 ) 和 EOFException ( 文件结束异常 ) 等,而 RuntimeException ( 运行时异常 ) 下包含了 NullPointerException ( 空指针异常 )、ArrayIndexOutOfBoundsException ( 数组越界异常 ) 等。InterruptedException ( 中断异常 ) 表示线程在等待时被中断。这些异常类构成了 Java 异常体系,用于处理程序运行时可能遇到的各种异常情况。
Spring 框架部分
请说说 Spring 的 IOC 即控制反转
IoC 即控制反转,一种设计思想,将原本在程序中手动创建对象的控制权和对象之间的相互依赖关系交由 Spring 的 IoC 容器来管理。 IoC 的机理,由 Spring 动态的通过 DI 依赖注入的方式来提供协作对象。 IoC 的底层,IoC 容器实际上就是个Map ( key,value ) , Map 中存放的是各种对象。 IoC 的原理,IoC 容器实际上就是个Map ( key,value ) , Map 中存放的是各种对象。 注入 IoC 方式有 构造器注入、setter 方法注入、根据注解注入。
请说说 DI 依赖注入
DI 依赖注入是一种设计模式,作用是根据依赖关系把某个对象所依赖的其他对象注入给这个对象,在项目运行中,IoC 容器动态的向某个对象提供它锁依赖的其他对象,是通过反射实现的依赖注入。
请说你对 AOP 面向切面编程技术的了解
AOP 技术是面向切面编程设计思想。在不入侵式的修改你的原有代码的基础上做一定的功能增强。可以减少系统的重复代码,降低模块间的耦合度,并有利于未来的拓展和维护。和面向对比呢,面向对象 OOP 是纵向的思想,而 AOP 是横向的。 AOP 的原理即动态代理,一种是 AOP 使用的 JDK 动态代理,是基于 Java 的反射机制,JDK 动态代理会在运行时生成一个实现了目标对象接口的代理类,代理类中的方法会拦截目标对象的方法调用,并在方法调用前后添加额外的逻辑。前提是这种方式要求目标对象必须实现接口。另一种是 Cglib 动态代理,是基于字节码生成库 ASM 实现的,它通过继承目标对象,生成一个子类作为代理类,并崇写目标对象的方法来实现代理功能,通过继承,CGLIB 动态代理可以直接调用目标对象的方法,而不需要像 JDK 动态代理一样通过反射调用。 AOP 技术的应用场景有,日志记录、权限控制、事务管理、性能监控和异常处理。
说说你对 Bean 的了解
说说 Bean 的创建方式
一种 BeanFactory 创建是,用 DefaultListableBeanFactory 类来编程式创建一个基本的 BeanFactory。另一种是 ApplicationContext 创建,用 ClassPathXmlApplicationContext、AnnotationConfigApplicationContext等,声明 xml 或 @Configuration 注解类。
说说 Bean 的生命周期
1. Bean 的实例化阶段
Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的,是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化。
2. Bean 的初始化阶段
Bean 创建之后还仅仅是个"半成品",还需要对 Bean 实例的 属性进行填充 、执行一些 Aware 接口方法、执行 BeanPostProcessor 方法 初始化属性值、执行 InitializingBean 接口的初始化方法、执行自定义初始化init方法。
该阶段是 Spring 最具技术含量和复杂度的阶段,Aop 增强功能,后面要学习的 Spring 的注解功能等、Spring 高频面试题Bean循环引用问题都是在这个阶段体现。
3. Bean 的完成 ( 使用 ) 阶段
经过初始化阶段,Bean就成为了一个完整的 Spring Bean,被存储到单例池 singletonObjects 中去了,即完成了Spring Bean的整个生命周期。
4. Bean 销毁阶段
当应用程序关闭或者需要销毁 Bean 时,Spring 容器会调用销毁方法来执行清理操作,可以通过配置文件、注解或实现特定接口来指定销毁方法,常见的销毁方法有释放资源、关闭连接等。
说说 Bean 是怎么装配的
什么是自动装配
自动装配的方式或者说注解
注入 Bean 的注解有 @Autowired、@Qualifier、@Resource 和 @Value。 @Autowired 用在字段、构造函数、Setter 方法上可根据被注解的属性的类型来自动装配依赖的 Bean 引用。 @Qualifier 通常与 @Autowired 结合使用,用于指定要注入的 Bean 的名称,通常用于解决歧义性问题。 @Resource JavaEE 提供的,用于在字段、Setter 方法上标注,可以根据属性的名称 ( 未匹配到就会根据类型 ) 来自动装配依赖的 Bean。 @Value 用于在字段、Setter 方法上、构造函数的参数上,通常为字段等注入简单的值,通常为 str 或 数值。
注册 Bean 注解有 @Component、@Controller、@Service 和 @Repository。 @Component 是最通用的注解,用于标识任何被 Spring 管理的组件 Spring 自动扫描并将其实例化为一个 Bean。 @Controller 用于标识控制层 ( Controller ) 组件,通常用于处理用户请求、 调用服务层并返回响应结果的组件。 @Repository 用于标识持久层 ( Repository ) 组件,通常用于与数据库 或其他持久化机制交互的组件,例如 DAO ( 数据访问对象 ) 现在很多项目不用它的原因就是有了 Mybatis 持久化框架提供的 @Mapping 或。
自动装配的优缺点
优点是可读性高、简洁直观,快速开发。 缺点是无法配置集中管理、各个注解散乱,配置信息难以发现和修改,代码耦合性增加。
自动装配的原理
首先就是 Bean 的查找,也就是 Bean 的扫描与注册,Spring容器启动时,会扫描配置文件或注解指定的类,将所有定义为Bean的类加载到容器中,并为每个Bean生成一个BeanDefinition对象 再就是依赖注入,当容器需要将一个 Bean 注入到另一个 Bean 中时,会根据注入的方式 ( 例如 byName、byType等 ) 以及 Bean 之间的依赖关系,从容器中查找匹配的 Bean,并将其注入到目标 Bean 中。
什么是手动装配
主要通过 XML 方式,通常指定拿来注入的 Bean 的
对比一下 Spring 的 Bean 和 Java Bean
首先说 Java Bean,Java Bean 是一种符合特定命名约定的Java类,用于封装数据,例如可以用于描述业务逻辑对象、数据传输对象(DTO)、数据访问对象(DAO)等。 再说 Spring 的 Bean,它是由 Spring 的 IoC 容器管理的对象实例,Spring Bean 可以是任何 Java 对象。可以是业务对象、数据访问对象、服务类、控制器等各种类型的对象,而 Spring 会负责管理这些对象的生命周期和依赖关系。
请说说你常用的 Spring 注解
关于注册 Bean 的注解,@Component、@Controller、@Service 和 @Repository。 关于注入 Bean 的注解,@Autowired、@Qualifier、@Resource 和 @Value。 @Autowired 和 @Resource 相比,@Autowired 先用类型匹配,未找到就用名字匹配,而 @Resource 正好相反。 关于核心配置 的注解 @Configuration、@PropertySource 还有导入扫描配置的注解等。@Configuration,用于标识一个类为 Spring 的配置类;@ComponentScan 用于指定 Spring 容器要扫描的包路径,以寻找标记为 @Component 的组件类,通常与 @Configuration 一起使用;@PropertySource 用于加载外部属性文件,可以在 Spring 配置类中使用该注解指定属性文件的位置;@Import 用于导入其他配置类;@Primary 根据英文翻译过来是重要的、主要的,当多个相同类型的 Bean 候选者存在时,使用该注解来指定一个首选的 Bean;@Profile 用于定义不同环境下的配置。 关于 AOP 的注解 @Aspect 切面。@Before 前置通知。@AfterReturning 后置通知。@Around 环绕。@AfterThrowing 异常通知。@After 最终通知。 关于声明式事务 的注解 @Transactional 事务类或事务方法。
什么是声明式事务
声明式事务的使用流程,首先开启声明式事务,然后 @EnableTransactionManagement 注解将它使用在配置类上,最后 @Transactional 注解用在具体的方法上面封装为事务。 声明式事务的优点,不需要在业务逻辑代码中入侵式的吸入事务代码,仅需配置文件中做相关的事务规则声明或通过 @Transactional 注解的方式,便可以将事务规则应用到业务逻辑中。声明式事务的缺点,最细粒度只能细到方法,而不能如编程式事务一样细致到代码块级别。 为什么禁止使用声明式事务,它基于 AOP 多个切面互相影响,粒度没达到要求,最小只能是方法,产生多余的性能开销。
@Transactional 注解的原理,本质是一个 AOP 的切面,它在方法执行前后应用事务逻辑,在方法执行前,会开启一个事务,在方法执行后,会根据方法的执行结果决定是提交事务还是回滚事务。
如何解决循环依赖
首先要说说什么是三级缓存
一级缓存 singletonObjects ( 单例完整对象缓存 ),存储已经实例化和初始化完成的单例 Bean 对象。 二级缓存 earlySingletonObjects ( 半成品对象缓存 ),存储已经实例化但还没有初始化完成的单例 Bean 对象,用于解决普通的循环依赖问题。 三级缓存 singletonFactories ( 工厂方法缓存 ),存储创建单例 Bean 对象的工厂方法,用于解决存在 AOP 代理时的循环依赖问题。
解决循环依赖的过程是怎样的
当 Spring 创建一个单例 Bean 对象时,首先会从一级缓存中查找是否已经存在该对象,如果存在则直接返回,如果不存在则继续创建。 在创建过程中,Spring会先调用Bean的构造方法,实例化一个空的对象,然后把这个对象放入三级缓存中,并且把创建这个对象的工厂方法也放入三级缓存中。 然后,属性注入和初始化,Spring 会对这个对象进行属性注入和初始化操作,如果在这个过程中发现有其他的 Bean 对象依赖于当前的 Bean 对象,那么就会从三级缓存中获取当前的 Bean 对象或者它的工厂方法,然后注入到其他的 Bean 对象中。 最后初始化完成,当当前的 Bean 对象完成了属性注入和初始化操作后,Spring 会把它从三级缓存中移除,并且放入二级缓存中,同时,Spring 会检查是否有其他的Bean 对象依赖于当前的 Bean 对象,如果有,则会从二级缓存中获取当前的 Bean 对象,并且注入到其他的 Bean 对象中。 最后,当所有的单例 Bean 对象都创建完成后,Spring会把二级缓存中的所有对象移动到一级缓存中,并且清空二级缓存和三级缓存。
什么是拦截器
拦截器与过滤器的区别
首先过滤器呢,属于 JavaWeb 的原生技术,所截取的资源是对所有请求过滤 ( 包括任何 Servlet、JSP、其他资源等 ),执行时机是早于任何 Servlet 执行。 然后拦截器,SpringMVC 框架技术,所截取的资源是只对进入了 SpringMVC 管辖范围的才拦截,主要拦截 Controller 请求,执行时机是晚于 DispatchServlet 执行。
如何自定义拦截器
实现 HandlerInterceptor 接口。
请说说请求响应时的常用注解
请求映射路径注解,@GetMapping 注解、@PostMapping 注解、@RequestMapping 注解。 请求数据接受注解,@RequestParam 注解、@ResponseBody 注解、@RequestHeader 注解、@PathVariable 注解、@RequestAttribute 注解。
响应处理注解 @ResponseBody 注解,@ResponseBody 是为了 @ResponseBody 注解 controller 层可以让其中方法返回 Dao 层对象时自动转为 JSON 数据返回 。@ResponseBody 加 @Controller 等于 @RestController。
控制器注解,@RestController 注解,标记类是一个 RESTful 风格控制器组件, 相当于 @Controller 和 @ResponseBody 的组合。
请说说 SpringBoot 注解
SpringBoot 核心注解 @SpringBootApplication 注解的组成,由 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan。 @SpringBootConfiguration 它是一个 @Configuration 实现配置文件的功能。 @EnableAutoConfiguration 开启自动配置的功能,帮助 Spring Boot 应用将所有符合条件的 @Configuration 配置都加载到当前 Spring Boot ,并创建对应配置类的 Bean ,并把该 Bean 实体交给 IoC 容器进行管理。 @ComponentScan 注解是 Spring 组件扫描注解,默认就会装配标识了 @Controller ,@Service ,@Repository ,@Component 注解的类到 spring 容器中。
SpringBoot 常用注解有 @SpringBootConfiguration 注解,用于将类标记为 Spring Boot 的配置类。 @SpringBootApplication 注解,Spring Boot 主应用程序类标记注解,包括了 @Configuration、@EnableAutoConfiguration 和 @ComponentScan 注解。 @EnableAutoConfiguration 注解,用于自动配置应用程序的配置。 @ConfigurationProperties 注解,用于绑定配置属性到 Java 类,以便在应用程序中使用强类型的配置属性。 @SpringBootTest 注解,Spring Boot 的集成测试注解,用于加载整个 Spring 应用程序上下文。
Java 并发编程部分
线程使用有哪些方式
方法一继承 Thread,它实现了 Runnable 即可运行接口,步骤是先自定义类继承 Thread,再重写 run 方法,创建自定义线程类对象,对象再调用 start 方法,个人认为不常用不好用,因为占用了一个继承位置。 方法二实现 Runnable,步骤是先实现 Runnable 接口,再重写 run 方法,将该自定义线程类对象作为参数构造一个 Thread 类获得另一对象,再的调用 start 方法。 方法三实现 Callable 接口,重写 call 方法,创建对象,将对象传入 FutureTask 构造器构造另一个类对象,用这个对象调用 start。 方法四继承 FutureTask 它实现了 RunnableFuture ,间接实现 Runnable 接口。步骤是先创建一个 Callable 任务,2. 创建一个 FutureTask,将 Callable 任务包装起来,再创建一个线程来执行 FutureTask 任务,等待任务执行完成,并获取结果。
Callable 对比 Runable,Callable 的 call 方法可获取线程返回值,run 无法获取返回值,call 方法可以抛出异常 run 方法不能抛出异常,只能捕获处理非受检查异常。实现 Callable 的方法效率比较低,当前线程会受阻。总体来说 Callable 更常用。
请说说 synchronized
说说你是如何使用 synchronized 的
synchronized 的基本使用有 同步代码块、同步方法和同步静态方法。 先说同步代码块 synchronized 括号 需要被线程共享的对象。很多时候是 this 这个对象作为被抢占的对象锁,表明该对象在任一时刻只能由一个线程访问。同步代码块它自由度非常高。静态方法中的同步代码块的互斥锁 ( 也就是 synchronized() 里面这个对象 ) 必须是 类名.class。 再说同步方法,用法是 访问修饰 synchronized 返回类型 方法名( 形参表 ) 方法体内通常就是临界区。整个方法体都会被同步,可能会无故扩大同步的范围,导致程序的执行效率降低。而且还会导致这个被抢占的对象锁只能是 this ,所以这种方式不如同步代码块常用。因为同步代码块可以出现在方法体中,它完全可以替代同步方法,所以同步方法的方式不是很推荐。 最后同步静态方法,用法是访问修饰 synchronized static 返回类型 方法名( 形参表 )。表示互斥锁为类锁 类.class ,类锁来说永远只有 1 把,同一个类的100 个对象,只有一个类锁;而对象锁是 100 个对象,100 把锁。解决了静态变量的线程安全问题。 用 synchronized 实现简单共享锁的方式,任何线程都共用的对象锁,若暂时找不到各个线程所共享的对象,但想要使它们同步,可以用一个字符串 ( "abc" ) 作为对象,所有线程必共享这个常量池的字符串。
聊聊 synchronized 对象锁的引用
synchronized 对象锁的引用问题,如果的对象引用发生改变,就可能会导致多个线程共享一把锁 ( 多个对象的引用相同 )。 synchronized 对象锁的引用问题解决方案,将对象锁 ( 锁对象 ) 定义为 final 就不会改变其引用,从而保证一直是一个对象,达成真正的共享锁。
说说 synchronized 的底层
synchronized 是 JVM 内部实现的一种可重入的互斥锁,当一个线程尝试获取一个已经被其他线程获取的 synchronized 锁时,该线程会进入阻塞状态等待其他线程释放该锁。在这个等待过程中,该线程会不断的尝试获取锁,这个过程被称为“自旋”,即不断重试直到获取到锁为止。 在 JDK 1.6 之前,synchronized 采用的是偏向锁和重量级锁两种机制。对于偏向锁,如果一个线程获取到了一个对象的锁,那么该对象的头信息中会存储这个线程的标识,后续该线程再次获取这个锁的时候就不需要竞争了。对于重量级锁,当多个线程需要争用同一个锁的时候,会进入到阻塞状态,此时需要等待其他线程释放锁才能继续执行。 synchronized 之锁膨胀,锁膨胀的发生时间是轻量级锁发生在退出 cynchronized 代码块时 ( 解锁时 ) 有一个线程 Thread-0 已经获取了轻量级锁对象 Object ,此时 Object 的 lock record 地址是 00,轻量级锁状态,而不是无锁状态,也就是被其他线程加轻量级锁了,新线程 Thread-1 来加轻量级锁的时候执行 CAS 操作将 Mark Word 的值恢复给对象头失败,进入锁膨胀。 在 JDK 1.6 之后,synchronized 引入了自适应锁,当线程竞争锁的次数较少时,锁会采用自旋的方式,而不是阻塞。如果线程竞争锁的次数达到一定阈值,就会升级为重量级锁,避免出现过多的自旋。 synchronized 之锁的自旋优化,重量级锁竞争的时候,可用自旋进行优化,如果当前线程自旋成功 ( 即这时候持锁线程已经退出了同步块,释放了锁 ) ,这时当前线程就可以避免阻塞。 synchronized 偏向锁,只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,以后只要不发生竞争,这个对象就归该线程所有。
什么是 ThreadLocal 它可以做什么
ThreadLocal 它是一种线程局部变量的存储容器类,是一个类,允许每个线程拥有自己独立的变量副本,从而实现了线程间的数据隔离。
可以使用 ThreadLocal 存储线程的上下文信息,例如用户身份信息、数据库连接、会话信息等,使得每个线程都可以独立地管理自己的上下文信息,避免了多线程环境下的数据竞争和同步问题。同时可以共享数据。或者做成多线程线程安全的单例模式。
我用 TreadLocal 来封装当前登录用户,当需要检验用户权限等时,就从ThreadLocal 中获取当前用户而无需从数据库中获取。
关于线程的内存模型
首先说说 Java 内存模型 JMM ( Java Memory Model,Java 内存模型 ) 。 JMM 的三大要素原子性是保证指令不会受到线程上下文切换的影响,可见性保证指令不会受 cpu 缓存的影响,还有有序性保证指令不会受 cpu 指令并行优化的影响。
关于线程中变量的可见性,例如主程序 ( 主线程 ) 对一个变量进行修改,但对另一个线程不可见。 两种方式解决可见性问题,其中一种方法是 volatile ( 易变 ) 关键字,解决可见性问题,可以为被同步的变量添加 volatile 关键字,使得线程读取该变量时只能从主内存中读取,而不是高速缓存中。还有一种解决可见性问题是,synchronized 圈住同步变量,但是它要创建 Monitor ,是比轻量的 volatile 更加崇量级的操作。 volatile 和 synchronized 对比结合可见性和原子性,首先 volatile volatile 保证了可见性,但无法保证原子性 ( 如 i++ 底层是 4 条指令,且本身也不具有原子性 ) 。而 synchronized 既可以保证可见性,又能保证原子性。 synchronized 与有序性,synchronized 无法避免内部的代码的底层指令崇排,例如当有同步变量逃逸 synchronized 范围时会导致无法保证内部的该变量涉及的操作的指令的有序性。
关于原子性,是无锁 ( 即乐观锁 ) 的思想。原子性 之 CAS CAS 就是 compareAndSet() ( 比较并设置值 ) 或 compareAndSwap() ( 比较并交换 ) 。 CAS 与 volatile,CAS 必须与 volatile 相结合才能做到读取到变量最新值然后比较,且 修改 / 交换。CAS 和 volatile 集合可实现无锁并发,适用于线程数少、多核 CPU 的场景下。 CAS 与 synchronized,CAS 无锁比 synchronized 加锁 效率更高,因为CAS 无锁情况下,即便崇试失败也会高速运行 ( 如循环 ) ,而 synchronized 则会因为获取锁失败,上下文切换,线程陷入阻塞。 CAS 的特点有不会阻塞、少的上下文切换还有多次崇试。 关于原子类型,内部使用了 CAS ( Compare-And-Set ) 的设计,比 CAS 方便,但原子类性没其 CAS 灵活。三种原子基本类型,原子整数类型,可原子地加减。原子长整数类型,可原子地加减。原子布尔类型,可原子地设置和获取。三种原子引用类型,AtomicReference 原子引用类型,可以用于执行原子的引用对象的设置和获取操作。AtomicMarkableReference 同时还可以关联一个布尔标记 ( mark ) ,通常用于一些带有标记的数据结构。AtomicStampedReference 同时还可以关联一个整数标记 ( stamp ) ,通常用于带有时间戳的数据结构。有三种原子数组,整型、长整型、引用原子数组类型。字段更新器 ( 或者叫原子化字段 ),整型、长整、任何类型的字段。 A-B-A 问题,主线程只能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况 ( 总结就是 A 改为 B,B 改为 A,即 ABA 问题 ) 。解决 A-B-A 问题,使用 AtomicStampedReference 为共享变量添加 一个时间戳 ( stamp ),可以观察到变量更改的次数。使用 AtomicMarkableReference 可以优化 AtomicStampedReference 的时间戳,因为无需关注到时间戳的更新次数,只需要关心它是否被更改过。 Unsafe 对象,提供了非常底层的,操作内存、线程的方法。Unsafe 对象不能直接调用,只能通过反射获得。
什么是死锁、活锁和锁的饥饿
什么是死锁
死锁现象是什么
死锁现象,就是,如两个线程分别都有一个 synchronized 相互嵌套另一个线程中的 synchronized , 一个线程的外层 互斥锁对象 是 A 执行/睡眠了若干时间 内层嵌套了一个同步 互斥锁对象是 B ; 另一个的线程的外层 互斥锁对象是 B 执行/睡眠了若干时间 内层嵌套了一个同步 互斥锁对象是 A,这种情况就叫做死锁 ( 互相占有着对方将要抢占的互斥锁对象,这种死锁情况, 既不报错也不抛出异常,极难发现,且会使运行时僵持不动,必须避免 )。
死锁的条件是什么
死锁的条件,四个条件:互斥条件、请求与保持条件、不可剥夺条件和循环等待条件。互斥条件是指至少有一个资源被标记为独占性。请求与保持条件是指一个进程或线程在持有某个资源的同时,又请求其他进程或线程所持有的资源。不可剥夺条件是指已经获得的资源不能被强制性地剥夺,只能在使用完毕后自愿释放。循环等待条件是指存在一个进程或线程的资源请求链,使得每个进程或线程都在等待下一个进程或线程所持有的资源。
你是如何定位死锁的
有三种方式 jps 工具、jconsole 工具还有 Linux。 jps 定位进程 id,再用 jstack 定位死锁。 jconsole ( Java 图形化控制工具 ) 。 Linux 定位死锁,如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
如何避免死锁
一是注意加锁和释放锁顺序一致。 还有就是尽量减小锁粒度,缩小锁的影响范围。 可以设置锁超时兜底机制。 避免锁嵌套,做锁的分离,避免一个操作获取多个锁。
什么是活锁
说说活锁现象
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
如何避免活锁
线程整体交错 ( 开发中随机睡眠时间等实现 ) 。
什么是锁饥饿
饥饿现象
线程饥饿就是一个线程由于优先级太低, 始终得不到 CPU 调度执行,也不能够结束。
什么情况会出现锁饥饿
顺序加锁解决死锁,可能出现饥饿现象。
说说线程池 也就是 ThreadPoolExecutor
首先线程池是一种用于管理和调度线程的机制,它维护一组可崇用的线程,以便在需要时执行异步任务。 线程池优点有这些,降低资源消耗,减少创建线程的资源消耗。提高响应速度,线程分工明确,避免过多的上下文切换。提高线程的可管理性,线程之间更容易调度以及消息的传输。 ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,int 的其余低 29 位表示线程数量。状态有这些,RUNNING 状态,高三位是 111,可以接受新任务,可以处理阻塞队列任务。SHUTDOWN 状态,高三位是 000,不可接受新任务,可以处理阻塞队列任务,虽说不会接收新任务,但会处理阻塞队列剩余任务。STOP 状态,高三位是 001 不可接受新任务,不可处理阻塞队列任务,不仅不会接受新任务会中断正在执行的任务,并抛弃阻塞队列任务。TIDYING 状态,高三位是 010,任务全执行完毕,活动线程为 0 即将进入终结。TERMINATED 状态,高三位是 011,是线程的终结状态。 线程池的参数有这么些,核心线程数、最大线程数、救急线程相关、阻塞队列、线程工厂还有拒绝策略。 核心线程数参数规定了核心线程创建的上限。 最大线程数参数规定了救急线程数,无法规定核心线程数。救急线程数 = 最大线程数 减去 核心线程数。可能最大线程数 = 核心线程数,救急线程数为 0。 救急线程相关参数有救急线程生存时间参数和救急线程生存的时间单位参数,当急救线程在该时间内不执行任务后,会被线程池结束。 阻塞队列参数,可能为无界的队列,如 Executors 类的 newFixedThreadPool 方法的 LinkedBlockingQueue 类型的阻塞队列参数来构造线程池。代表任务可以无限 ( 其实 LinkedBlockingQueue 有长度上限 ) 的存在阻塞队列中,通常这样救急也会为 0。阻塞队列可能为有界的队列,如 ThreadPoolExecutor 的构造方法的 BlockingQueue 参数。 线程工厂参数就是可以自定义的线程工厂,可以自定义线程名,自定义线程数,可自定义优先级,还有是否为守护线程。还有额外功能上下文加载器和异常处理器等。 拒绝策略参数呢,4 种基本的 jdk 拒绝策略有中止策略 即 抛出异常、一是调用者运行策略,二是让调用者运行任务策略、三是放弃本次策略、四是放弃队列中最早最旧的任务,本任务取代它。还可以自定义拒绝策略实现RejectedExecutionHandler 接口,将自定义拒绝策略对象传递给线程池构造方法。 线程池的运作,最开始,按需创建,线程池中没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。再进入任务排队与等待阶段,当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。救急机制,如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize 减去 corePoolSize 数目的线程来救急。还有执行拒绝策略,如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略,拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现 。最后节省资源,结束救急线程,当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。 线程池创建的核心工作是提交任务,提交任务就是将一个任务添加到线程池中以供执行。任务生命周期是这样的,首先接受任务,线程池接收提交的任务,这可以是一个实现了 Runnable 或 Callable 接口的任务对象。然后任务可能被放入阻塞队列,如果没有核心线程可以来执行该任务,该任务会放入阻塞队列,等待线程来获取然后执行。任务也可能会被线程执行,有核心线程且其空闲,那么会被执行,在任务队列中被线程获取也会执行,这包括执行 Runnable 任务的 run 方法或执行 Callable 任务的 call 方法。再就是任务结果返回,如果任务是一个 Callable 任务 ( 即有返回值的任务 ) ,线程池会将任务的结果封装在 Future 对象中返回,通过 Future 对象,您可以随时获取任务的执行结果。在这期间当异常发生时,线程池通常会捕获并记录异常,以避免线程因异常而终止。处理执行任务的异常可以用 try-catch 还有 Future 获取 submit 方式获取异常。最后任务执行完成,一旦任务执行完毕,线程会返回线程池,并可能被崇用来执行其他任务。 提交任务的方式有 submit 提交指定任务、invokeAll 是提交所有任务、invokeAny 哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消。 关闭线程池用 shutdown 还有 shutdownNow 。
谈谈 CountdownLatch
我对 CountdownLatch 仅有了解,它是可以用来实现线程的同步调度,可以实现同步等待多线程准备完毕,同步等待多个远程调用结束。 CountdownLatch 的基本使用有用其构造参数来初始化等待计数值,await() 方法等待计数归零,countDown() 方法用来让计数减一。
聊聊 ConcurrentHashMap
ConcurrentHashMap 它和 HashMap 有共同的接口,它不是继承 HashMap ,而是与 HashMap 有共同接口,相当于同门师兄。 ConcurrentHashMap 它的线程安全和并发状况,第一,分段的段粒度的锁,内部将哈希表分成了多个段 ( Segment ) ,每个段可以看作是一个独立的哈希表,它们之间相互独立,每个段都有自己的锁,因此多个线程可以同时访问不同的段,提高了并发性能。第二,"桶" 粒度的锁,每个段内部包含一个或多个桶,每个桶就是一个键值对的集合,在进行插入、删除、查找等操作时,线程只需要锁住该段内的一个桶,而不是整个哈希表,从而减小了锁的粒度,提高了并发性能。第三,具有 CAS 并发控制机制,在执行某个操作之前,它会先检查数据是否被其他线程修改,如果没有被修改,则可以执行操作,否则需要进行崇试,ConcurrentHashMap 底层封装了一些方法可以原子性地执行 get 和 put ,例如 computeIfAbsent() 方法。第四,无锁读取,读取操作通常不需要锁,因此多个线程可以同时进行读取操作,不会阻塞。 ConcurrentHashMap 和 HashMap 对比,相比之下 ConcurrentHashMap 有两个特殊的扩容机制一个是转移节点,另一个是帮助扩容,转移节点是指 ConcurrentHashMap 使用了一种叫做转移节点 ( ForwardingNode ) 的特殊节点来标识正在进行扩容的段,当其他线程访问该段时,会被转移到新数组中进行操作。帮助扩容,ConcurrentHashMap 使用了一种叫做帮助扩容 ( Help Transfer ) 的机制,当一个线程发现某个段正在进行扩容时,会主动参与到扩容过程中,从而加快扩容速度。特殊的树化和退化,加锁树化与退化,ConcurrentHashMap 在进行树化和退化时,会对整个链表或红黑树加锁,而不是对单个节点加锁,从而避免了死锁或数据不一致的问题。ConcurrentHashMap 在进行树化和退化时,会检查当前线程是否被中断,如果是,则会抛出 InterruptedException 异常,从而响应中断请求。 ConcurrentHashMap 的底层原理是这样的,Java 8 数组 ( 即内部 Node 类型) + ( 链表 Node 或 红黑树 TreeNode ) 数组称为 ( table ) ,链表和红黑树称为 ( bin )
请说说你了解的并发设计模式
我了解的两种同步设计模式
什么是保护性暂停
这是一种同步模式,适用于一个线程需要等待另一个线程完成工作的情况。它通常用于实现类似消息传递的功能。例如,Java 中的 Thread 的 join 方法和 Future 接口就是基于这种模式实现的。使用这个模式时,我们需要确保线程间的通信是通过共享的对象 ( GuardedObject ) 来完成的。
什么是 Balking ( 犹豫 )
Balking ( 犹豫 ) 模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回,这就是 Balking 犹豫模式。
我了解的两种异步设计模式
生产者 / 消费者模式
生产者 / 消费者模式这是一种异步模式,它通过一个中间的消息队列来解耦生产者和消费者,使得两者可以独立地工作。Java 的 BlockingQueue 接口就是这种模式的一个实现。生产者/消费者模式允许系统在生产数据和处理数据之间保持平衡,同时提高了系统的吞吐量和响应性。
WorkThread 工作线程
让有限的工作线程 ( Worker Thread ) 来轮流异步处理无限多的任务,它的典型实现就是线程池,JDK 中 ExecutorService 是这种模式的一个例子。
说说 sleep 和 wait 的区别
首先说 sleep 是 Thread 的静态方法,它不需要和 synchronized 绑定使用,sleep 睡眠后不会释放锁。 而 wait 呢,是 Object 的方法,需要配合 synchronized 使用,wait 等待后会释放对象锁。
说说 interrupt 和 interrupted 两者的区别
先说 interrupt() 方法,它会中断线程的睡眠 sleep、等待 wait、合并 join ( WAITING 或 TIMED下划线WAITING ) ,立即抛出 InterruptedException 异常,从而导致线程退出等待状态 ( 唤醒 ) ,会吧线程标记为 ( INTERRUPTED ) 中断状态 ( 打断标记 ) ,同样不会立即执行。它不会切换六态中的任何一个状态,它不切换线程状态。 interrupted() 方法呢,它会判断是否被打断,会清除打断标记,因为打断标记是布尔值,可以用它来控制线程的是否继续执行,实现逻辑上的 interrupt() 打断运行状态的线程。 还有一个 静态的 isInterrupted() 方法是判断是否被打断,且不会清除打断标记。
说说 volatile 关键字
volatile 关键字的作用是实现读写屏障,对 volatile 变量的写指令 ( 例如修改代码 ) 后会加入写屏障,对 volatile 变量的读指令 ( 例如获取代码 ) 前会加入写屏障。 volatile 关键字它的底层实现是基于内存屏障的。 volatile 开启的写屏障的情况下保证了该屏障之前的所有共享变量的改动都会同步到主存,且保证了屏障之前的所有共享变量不会进行指令崇排。 volatile 开启的读屏障的情况下保证在该屏障之后,对所有共享变量 ( 不管是否为volatile ) 的变量读取,加载的是主存中最新数据,而不是缓存中可能的旧数据,读屏障还保证了屏障之后的代码不会崇排序到读屏障前。
请说说各种锁的性质
什么是可重入锁
允许同一个线程多次获得同一把锁,避免死锁情况发生。常见的实现是通过给每个线程关联一个计数器来实现。
什么是可打断锁
允许线程在等待锁的过程中被其他线程打断而退出等待状态,防止线程因为等待锁而长时间阻塞。
什么是独占锁
同一时刻只允许一个线程持有锁,其他线程需要等待锁释放后才能获取。
什么是公平和非公平锁
公平锁是指多个线程按照它们发出请求的顺序获取锁,非公平锁则是允许随机地选择线程获取锁,不考虑请求的先后顺序。
什么是读写锁
读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读操作之间不会互斥,写操作与任何其他操作(读操作或写操作)都互斥。
什么是自旋锁
自旋锁是一种轻量级的锁,当线程尝试获取锁时,不会立即阻塞,而是通过循环的方式等待锁的释放。适用于锁被持有时间短、线程并发度高的场景。
什么是互斥锁和共享锁
互斥锁(独占锁)只允许一个线程持有锁,其他线程需要等待。共享锁允许多个线程同时持有锁,适用于读多写少的场景。
什么是阻塞锁和非阻塞锁
阻塞锁在获取锁失败时会让线程进入阻塞状态等待锁释放,而非阻塞锁则不会让线程进入阻塞状态,而是通过忙等或者轮询等方式尝试获取锁。
乐观锁 和 悲观锁 ( 其实是设计思想 )
乐观锁假设多个线程之间不会发生冲突,直接进行操作,只在更新时检查是否有其他线程修改了数据;悲观锁则是假设多个线程之间会发生冲突,因此在读写操作时会进行加锁操作。
MySQL 和持久化部分
说说你设计数据库时你会注意什么
表数据内容选择
表必备三字段 : id 主键类型为 bigint unsigned 单表,步长为 1 的自增。create 主动创建时间,modified 被动更新时间 都是 datetime 类型。
范式设计和反范式
三大范式
第一范式,1NF 的要求是,确保数据库表字段的原子性。 第二范式,2NF 的要求是,首先满足第一范式,一是表必须有主键;二是非主键列必须完全依赖于主键,而不能只依赖于主键的一部分。 第三范式,3NF 的要求是,首先满足第二范式,另外非主键列必须直接依赖于主键,不能是传递依赖。也就是说不能存在这种情况:非主键列 A 依赖于非主键列 B,然后非主键列 B 依赖于主键。
子主题
表数据类型如何选择
表示是否的字段,命名必须 is下划线某某某,类型必须是 unsigned tinyint ( 1 表示是,0 表示否 ) 。 表示小数的字段,小数类型为 decimal,禁止使用 float 和 double,float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果,如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。 表示字符的字段,如果该记录该字段的长度几乎相等就用 char 定长。varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,超过 5000 的字串的字段就不用 varchar 了,定义字段类型为 text。且 独立出来一张表,用主键来对应,避免影响其它字段索引效率。 整型字段按需求选择。 实数 ( 浮点 ) 数字段的选择,例如 9.9 ¥价格,存入 9900 ,单位分,数据类型 整型,整数通常是最佳的数据类型,因为它速度快,并且能使用 AUTO下划线INCREMENT。 主键类型为 bigint unsigned。 表示时间字段,datetime。
什么情况下选择字段自增
字段是主键或唯一索引,并且不需要与业务逻辑相关联,因为只是数字,没有业务逻辑含义。 字段不需要保证全局唯一,并且可以容忍缺失或跳跃,事务回滚、删除操作、并发插入等,会导致自增字段产生缺失或跳跃。 字段不需要保证有序性,并且可以容忍乱序或崇排序。 字段不需要保证可读性,并且可以容忍无意义或随机的值。
什么情况下使用索引什么情况不使用
适合用索引的情况,库中数据量大,数据库数据量达到一定程度,用索引比较划算,还有常查字段常用的字段上创建索引。 不使用索引的情况,项目上线时不用索引,不要在线上环境去创建索引,也就是再有用户在使用此系统时,创建索引会锁表,线上数量大会导致其他所有接口不可用,导致系统性崩溃。 不常查且常改的字段,不常使用的列,或经常做插入、修改操作的列,不适合加索引。
说说 InnoDB 和 MyISAM 引擎的区别
InnoDB 引擎,即支持行锁,也支持表锁,支持事务,不支持全文索引,支持外键。 MyISAM 引擎,不支持行锁,只支持表锁,不支持事务,支持全文索引,不支持外键。
说说 MySQL 的并发场景及其线程安全问题
MySQL 的并发场景有三种,读读、读写和写写。 读读,不会存在任何问题,不需要并发控制。 读写,有线程安全问题,会产生脏读、幻读、不可重复读。 写写,会产生丢失更新的问题。
线程不安全与低效问题,有这么几种,丢失更新、更新异常、读-写冲突、写-写冲突、死锁和锁竞争。 丢失更新 ( Lost Update ) ,是在并发事务中,多个事务同时修改同一数据,并且后提交的事务覆盖了先前提交的事务所做的修改,导致先前的修改丢失。 更新异常 ( Update Anomaly ) ,是在并发环境下,如果多个事务同时对一个数据进行读取、修改和写入,可能会导致部分事务的修改被覆盖,从而导致数据不一致。 读-写冲突 ( Read-Write Conflict ) ,一个事务正在读取数据,而另一个事务正在尝试修改相同的数据,根据隔离级别,这可能会导致阻塞或产生其他问题。 写-写冲突 ( Write-Write Conflict ) ,多个事务同时尝试对同一数据进行写操作,由于数据库通常只允许一个事务进行写操作,所以其中一个事务必须等待另一个事务完成。 死锁 ( Deadlock ) ,死锁呢是指多个事务之间互相等待对方释放资源,导致所有事务都无法继续执行,这种情况下,需要数据库管理系统采取措施来检测和解决死锁。 锁竞争 ( Lock Contention ) ,多个事务试图同时获取相同资源的锁,导致竞争和性能下降。
请说说什么是 MVCC
MVCC 是 Multi-Version Concurrency Control ( 多版本并发控制 ),它是一种用来解决读-写冲突的无锁并发控制机制。逻辑上维持一个版本,使得读写操作没有冲突。 目的是用来解决数据库并发场景中并发处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。 MVCC 它实现了 InnoDB 引擎的行锁。 MVCC 的隔离级别,就是 InnoDB 默认的隔离级别是 RR ( REPEATABLE READ ) 可重复读。 MVCC 的当前读和快照读的概念,首先当前读,是读取的数据是记录的最新版本,包括 update、delete、insert、select、lock in share mode、select for update。快照读,读取的数据是记录的历史版本。 MVCC 的原理机制是这样的,MVCC 实现了一个隐藏列的功能,为 InnoDB 引擎下的每行数据添加隐藏列,隐藏列中包含了本行数据的事务 id、指向 undo log 的指针。MVCC 还是基于 undo log 的版本链的,每行数据的隐藏列中包含了指向 undo log 的指针,而每条 undo log 也会指向更早版本的 undo log,从而形成一条版本链。MVCC 的 ReadView 参照来恢复版本,是指事务 ( 例如事务 A ) 在某一时刻给整个事务系统 ( t-r-x下划线sys ) 上打快照,之后再进行读操作时,会将读取到的数据中的事务 id 与 该系统 t-r-x下划线sys 的快照比较,从而判断数据对该 ReadView 是否可见,即对事务 A 是否可见。通过隐藏列和版本链,MySQL 可以将数据恢复到指定版本,但是具体要恢复到哪个版本,需要根据 ReadView 来定。
请说说事务的四大特性 ( A-C-I-D )
A 即原子性
对应的 MySQL 原理
是 undolog 日志。
C 即一致性
对应的 MySQL 原理
是最根本的要求,AID 共同保证了一致性。
I 即隔离性
对应的 MySQL 原理
是 锁 + MVCC。
D 即持久性
对应的 MySQL 原理
是 redolog 日志。
说说事务读写问题有哪些
有 脏读、不可重复读和幻读三种读写问题。 脏读,是指在一个事务中,读取了另一个未提交事务的数据。脏读的过程是这样的,首先,事务 A 开始,对某一行数据进行了修改,但尚未提交 ( 后续会继续修改或回滚 ) ,然后,事务 B 在事务 A 未提交之前,读取了事务 A 修改的数据,最后,事务 A 回滚或提交后,事务 B 读取的数据可能是不一致的或无效的。 不可重复读,是指在一个事务内,多次读取同一行数据,但在事务执行期间, 其他事务对该行数据进行了修改,导致多次读取的结果不一致。不可重复读的过程是这样的,首先,事务 A 开始,读取某一行数据的值,然后,事务 B 在事务 A 未提交之前,对同一行数据进行了修改并提交,最后,事务 A 再次读取同一行数据, 发现其值已经发生了改变,与第一次读取时的结果不一致。 幻读,是指在一个事务中,多次执行同一个查询,但在事务执行期间,其他事务插入或删除了符合该查询条件的数据,导致多次查询的结果不一致。幻读的过程是这样的,首先,事务 A 开始,执行一个查询语句,返回一组数据,然后,事务 B 在事务 A 未提交之前,插入或删除了符合事务 A 查询条件的数据,最后,事务 A 再次执行相同的查询语句,发现返回的数据集发生了变化,出现了新的数据或者缺失了原有的数据。 现在 你 可以 对比一下 不可重复读 和 幻读 了。
事物隔离级别有哪些
事务隔离级别有四种,未提交读、已提交读、可重复读和可串行化。 首先呢,未提交读 ( Read Uncommitted ) 是最低的隔离级别,允许一个事务读取另一个事务未提交的数据,可能会导致脏读、不可重复读和幻读问题。 其次,已提交读 ( Read Committed ) 是保证了一个事务只能读取另一个事务已经提交的数据,避免了脏读问题,但可能出现不可重复读和幻读问题。 然后,可重复读 ( Repeatable Read ) 是 MySQL 的默认事务隔离级别,确保一个事务在多次读取同一数据时,结果始终一致,避免了脏读和不可重复读问题但是可能会出现幻读问题。 最后的可串行化 ( Serializable ) 是最高的隔离级别,确保一个事务完全独立运行,不会受到其他事务的影响,避免了脏读、不可重复读和幻读问题,但可能会导致并发性能下降。
请对比一下行锁和表锁
什么是表锁
表锁它是锁定的整个表而不是表中的特定行。 表锁的触发情况有三种,DDL 操作 ( 也就是数据定义语言操作 ) 、全表扫描还有表级锁定请求。也就是说在没使用索引或索引失效时发生。 主要的表锁有表级共享锁和标记排它锁。
什么是行锁
行锁呢它是在行级别对数据库表的数据进行锁定。 行锁是基于索引的而不是基于整个表的,当事务锁定一行记录时,实际是锁定了该行记录在索引中的对应位置,也就是通过给索引上的索引项加锁实现,这样来避免不必要的锁竞争。 三种行锁算法,记录锁、间隙锁还有临键锁。记录锁,是用于锁定表中的单个记录,当一个事务锁定了某个记录时,阻塞其他事务的写操作,但是不会阻塞读操作。记录锁,是用于锁定一个范围,但不包括被锁定的行,间隙锁可以阻塞其他事务对该范围内的插入操作,避免幻读的问题,间隙锁主要用于保证范围查询的一致性,当一个事务加了间隙锁时,阻塞其他事务的写操作,但是不会阻塞读操作。临键锁,是结合了记录所和间隙锁的特性,用于锁定索引范围内的记录和记录本身。 行锁的触发,事务两种主动的获取行锁的情况,一种显式的获取行锁 SELECT ... FOR UPDATE 用于读取数据并为其获取排他锁 ( 写锁 ) ,确保其他事务无法同时获取写锁或共享锁。还有一种隐式的获取行锁,通常发生在默认的隔离级别 可重复读 REPEATABLE READ 下,还有进行更新操作时会获取行锁,即增删改。最后就是外键约束和唯一性检查也会获取行锁。
请说说 MySQL 有哪些日志
三大日志,undolog、redolog 还有 binlog。 undolog 是回滚日志,记录的是数据的历史版本信息,用来保证原子性和 MVCC。 redolog 是前滚日志,基本的持久化 ( 内存到磁盘 ) 的过程是随机的数据读写 ( 速度慢 ) ,而 InnoDB 的持久化是将内存数据顺序读写 ( 速度快 ) 到 redolog 中,当需要数据恢复时才会从 redolog 恢复到磁盘中,只会减小数据丢失的程度和概率 ( 用到预写日志机制,很多数据介质会用到 ) 。是 InnoDB 存储引擎独有的。 binlong 是二进制日志,主要进行主从同步,主节点 ( master ) 中进行 DML ( 更改 ) 操作就会在其中生成 binlog 文件。
其他日志例如,slowlog、errorlog 还有 relaylog 等日志。 slowlog 是慢查询日志,设定时间标准,把执行时间超过时间标准的 SQL 记录到 slowlog 中,方便日后优化调整。 errorlog 是错误日志,记录数据库进程中的错误信息。 relaylog 是中继日志,在 从节点 ( slaver ) 中记录 binlog 的暂存信息。
日志的两阶段提交
两阶段提交的总体过程,DML 操作 到 写 redolog ( prepare 准备阶段 ) 到 写 binlog 到 redov 置为 commit 提交阶段。其实无论 binlog 和 redolog 孰先孰后都会导致在主从情况下数据不一致。 第一阶段呢,是在事务执行期间,当事务需要锁定一行记录时,InnoDB会尝试获取该行的锁,如果锁可用,则事务可以继续执行,如果锁不可用,则事务会进入等待状态,直到锁可用。 然后第二阶段呢,在事务执行过程中,当事务不再需要某个行的锁时,InnoDB 会释放该行的锁,使其他事务可以获取到该行的锁。 这其中涉及到锁冲突检测,InnoDB 会检测是否存在死锁 ( Deadlock ) ,即多个事务互相等待对方持有的锁而无法继续执行的情况,如果检测到死锁,InnoDB会选择一个事务进行回滚,以解除死锁。
InnoDB 引擎 B-Tree 四种索引
四种基本的索引,其他索引由这些索引派生而来,这四种基本的索引是单值索引、复合索引、唯一索引还有主键索引。 单值索引是在单个列上创建的索引,用于加速对该列的等值查询,它是最基本的索引类型,适用于频繁查询的单个字段,适用率远小于多列或说复合索引。 复合索引是在多个列上创建的索引,用于加速涉及这些列的查询,复合索引可以优化多列的等值查询和范围查询,适用于频繁查询的多个字段。 唯一索引在列上创建唯一性约束,确保列中的值唯一,它通常用于主键列或需要保持唯一性的列,适用于主键自动内含创建唯一索引,除此以外根据需要建立其他唯一索引。 主键索引是在主键列上创建的唯一索引,主键是用于标识表中唯一记录的列,适用于确定表的主键字段。
除四种基本索引外,还有根据他们所派生出来的索引形式。 单值索引 和 复合索引 派生出的前缀索引 和 覆盖索引 的形式,前缀索引是在列值的前缀上创建的索引,它可以在保持索引效益的同时减少索引占用的存储空间。适用于创建长文本字段索引,且该各记录的该字段的一些前缀发生重复。覆盖索引是指一个索引包含了所有查询需要的数据 ( 包括SELECT语句中的选择条件和查询的输出列 ) ,当你查询覆盖索引时,数据库引擎可以直接从索引中获取所需的数据,而不必回到原始的数据表中查找,这可以显著提高查询性能,因为减少了对数据表的 I/O 操作。适用于当你的查询需要的列都包含在某个索引中且该查询频率高到一定程度时。 唯一索引 和主键索引派生出的外键索引,外键索引是在外键列上创建的索引,用于实现表之间的关系。外键关系可以保持数据的完整性,它不是独立的索引类型,它是主键索引结合唯一键索引,适用于确定该表与另一个表的关联字段。
请说说什么是聚簇索引
聚簇索引,是将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据。 一般来说,聚簇索引在表中指的就是主键索引,但是有的表很特殊,没有主键,就会像 oracle 一样创建 row下划线id 作为主键。
非聚簇索引可称为辅助索引,将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置。 InnoDB 中,可以理解为在聚簇索引之上创建的索引就是非聚簇索引,比如复合索引、前缀索引、唯一索引等。
什么是复合索引或问 什么是最左前缀原则
最左前缀原则是什么,对于复合索引 ( 多列组成的索引 ) ,数据库只能在查询条件中使用索引的最左边的一部分列来进行索引匹配。
最左前缀原则的情况讨论如下。 基于 ( a b c ) 的顺序的列的查询能否利用索引,调整顺序,可以。 基于 ( a c b ) 的顺序的列的查询能否利用索引,调整顺序,可以。 基于 ( c b ) 的顺序的列的查询能否利用索引,不包含最左前缀,不可以。 基于 ( b c a ) 的顺序的列的查询能否利用索引,调整顺序,可以。
什么是倒排索引
倒排索引是一种用于快速查找某个单词、词组或者词项 ( Term ) 出现在哪些文档中的数据结构。 倒排索引常用于全文搜索引擎中,能够快速定位文档中包含某个特定词语的位置。 是将文档集合中每个文档中的词项与其所在文档的映射关系进行反转,即按照词项来索引文档,而不是按照文档来索引词项。 倒排索引的底层,首先分词,对文档集合中的每个文档进行分词,将文档拆分为词项。然后构建索引,对每个词项构建索引,即记录该词项出现在哪些文档中以及在每个文档中的位置。最后查询,当进行查询时,根据查询词项在倒排索引中找到包含该词项的文档列表,然后根据需要进一步检索这些文档,以确定其相关性。 倒排索引的组成,通常由两部分组成 : 词典 ( Dictionary ) 和倒排列表 ( Inverted List ) 。首先词典,词典是所有文档中出现的唯一词项的列表,通常以字典树或哈希表的形式存储,每个词项对应一个唯一的标识符。然后呢倒排列表,对于词典中的每个词项,都有一个倒排列表,用于记录包含该词项的文档列表以及每个文档中该词项的位置信息。
你会如何优化慢查询
首先我会先查看慢查询,explain指令,还有 show processlist 指令来查看慢查询。 然后就优化查询环节,先确保查询语句使用了合适的索引,可以通过使用EXPLAIN命令来查看查询的执行计划,判断是否使用了索引,避免使用全表扫描,尽量使用索引来定位数据,减少查询返回的数据量,只选择需要的列,避免不必要的数据传输和处理,尽量避免使用 SELECT *,而是明确列出需要的列,避免在查询中使用函数,尽量将函数的计算移到应用层。 再看看怎么优化一下表结构,根据查询的特点和频率,考虑使用分区表、分表等技术来提高查询性能。 再看看如何优化一下索引,分析查询的访问模式,选择合适的索引策略,包括单列索引、组合索引、覆盖索引等,定期分析和优化索引,删除不再使用的索引,避免过多的索引导致性能下降。 还可以避免所冲突来优化慢查询,合理使用事务和锁机制,避免长时间的锁等待,尽量使用读写分离,将读操作和写操作分散到不同的服务器上,减少锁冲突。 还能优化数据库配置,根据实际情况,调整数据库的缓冲区大小、连接数等配置参数,以适应应用的负载。 还有就是超出 mysql 的范畴用缓存来优化慢查询,对于一些频繁查询的结果,可以考虑使用缓存来避免重复查询数据库。 再就是定期维护和监控了,定期分析和优化慢查询日志,找出潜在的性能问题,监控数据库的性能指标,及时发现并解决性能问题。
查询语句加载顺序是怎样的
第一个加载 FROM,第二个加载 ON,第三,同时加载 JOIN 和 WHERE,第四,加载 GROUP BY,第五,加载 HAVING,第六,加载 SELECT,第七,加载 ORDER BY,最后,加载 LIMIT。
Mybatis 二级缓存
一级缓存是基于 SqlSession 的本地缓存的,是默认开启的缓存机制,位于 SqlSession 的内部,生命周期与 SqlSession 生命周期一致。 先说说一级缓存的查询缓存是当执行查询语句时,查询结果会被缓存在一级缓存中,下次相同的查询语句可以直接从缓存中获取结果,而无需再次查询数据库。 还有一级缓存的清空缓存机制,因为其一级缓存的生命周期与 SqlSession 的生命周期一致,当 SqlSession 关闭或进行了更新操作 ( 如插入、更新、删除 ) 时,一级缓存会被清空。 还有一级缓存的缓存失效,一级缓存,在进行 DML 操作后,会使得缓存失效,也就是说 Mybatis 知道我们对数据库里面的数据进行了修改,所以之前缓存的内容可能就不是当前数据库里面最新的内容了。 一级缓存多会话不共享,一级缓存只针对于单个会话,多个会话之间不相通。
二级缓存是基于 Mapper 的全局缓存的,二级缓存不像一级缓存是默认开启的,需要手动开启。 二级缓存是基于命名空间的,二级缓存是基于命名空间 ( Mapper 接口的命名空间 ) 级别的缓存,每个命名空间对应一个独立的二级缓存,不同的 Mapper 接口查询出的数据会被放在各自对应的缓存 ( 一个命名空间对应一个缓存对象 ) 中。 二级缓存在查询缓存是的顺序是这样的,开启二级缓存后,查询执行流程为 : 二级缓存 到 一级缓存 再最后到 数据库。 二级缓存是全局缓存,多个会话共享,但是只有在第一个会话执行了更新操作并将数据写入二级缓存后,第二个会话才能从二级缓存中获取到更新后的数据。
防止 SQL 注入的方法,也就是 井号 大括号 和 美元符号 大括号的区别
SQL 注入是这样的,例如登录请求底层 SQL 就是在 WHERE 中用输入的密码和数据库密码进行判断,但是如果没有防止 SQL 注入的措施,就可以通过输入密码时在密码后面加上 OR 1=1 这样用就成立的语句,导致无论输入密码是否正确都能成功登录。 井号 大括号的占位符,使用预编译的方式,底层是将占位符安全地替换为 ( JDBC占位符 ) 后,再用参数替换,防止 SQL 注入。 美元符号 大括号,直接拼接 sql 语句,无法防止 SQL 注入,一般全部用 #{},但是在替换动态表名很有用。
Redis 缓存部分
Redi 数据类型
简单说说 Redis 五种基本数据类型
五种数据类型是这么几种,String、List、Set、Hash 还有 Sorted Set ( 也就是 ZSet ) 。 String 类型存储的是字符串,也是动态的变长字符串,它的底层类似 Java 的 ArrayList,并且采用预分配冗余空间的方式来减少内存的频繁分配。 List 类型是有序的字符串集合,可以在列表的两端进行插入和删除操作,List 类型可以做异步队列、消息队列、最新消息的存储和排行榜。 Hash 类型是键值对的集合,Hash 类型可以存储多个字段和对应的值 ( 和 field ) 。 Set 类型无序的字符串集合,且不允许重复元素,所以可以实现判断一人一单,一人一奖,一人一赞的业务功能。因为是集合,所以可进行交、并、补操作,所以还可实现 共同好友,共同关注。 Sorted Set 类型是有序字符串集合,Sorted Set 类型每个 value 都可以关联一个分数 score,Sorted Set 类型可根据分数排序做成热度排行等。
说说你用过的 Redis 特殊数据类型
Streams 流类型,它可以可以存图片,只是测试过,没用融入进项目。 HyperLogLogs 类型,可用于技术估计,也只是测试过。 Geospatial 类型,也就是 G-E-O 数据结构,可用于计算经纬度距离。 Bitmap 类型,位图,可用于实现签到。 Pub/Sub 类型,可用于发布订阅。
请说说缓存三剑客 缓存 ( 失效 ) 问题
什么是缓存穿透
缓存穿透就是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会达到数据库。 缓存穿透会导致一些严重的后果,例如,服务器的负载不均衡,一些服务器可能会受到更多的请求压力,而其他服务器却很少被使用,这可能降低系统的整体性能。再例如,数据一致性问题,因为多个线程频繁的操纵数据库和缓存,就会出现错位的访问导致数据不一致 。还有就是会导致内存和数据库高压,频繁访问导致的。 缓存穿透 的常见解决方案,主要有两种,一 缓存空对象,二 使用布隆过滤器,当客户端在缓存未命中和数据库中未查询到时,为这个 key 赋 null 存入缓存,它好在实现简单,维护方便,但是缺点是额外的内存消耗,各种各样的空值 ( 可以加一个 TTL 有效期 ) 。可能造成短期的不一致,因为通常选择的先更新数据库,后删缓存,导致数据库其实刚赋予值,而缓存中是 null。布隆过滤器是指在客户端和 Redis 之间建立一个过滤器,好处是内存占用较少,没有多余 key,实现复杂,存在误判 ( 依然存在穿透 ) 可能。
什么是缓存雪崩
是指在同一时段大量的缓存 key 同时失效或 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。雪崩的后果还是比较严重的,因为雪崩的本质还是大量请求访问底层数据库,所以和缓存穿透的后果一样。
避免雪崩的方案有这些,基本上都是侧面的缓解,一种是给不同的 Key 的 TTL 添加均匀的随机值避免缓存雪崩,解决 Key 的突然大量失效。还有一种利用 Redis 集群提高服务可用性避免缓存雪崩,形成 Redis 集群主从,Redis 哨兵。再就是,给缓存业务添加降级限流策略避免缓存雪崩。最后还有给业务添加多级缓存避免缓存雪崩,例如 Nginx 缓存 到 Redis 缓存 再到 JVM 本地缓存 最后到 数据库建立缓存。
什么是缓存击穿
也叫热点 Key 问题,就是一个被 高并发访问并且缓存崇建业务较复杂 的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。会带来非常非常严重的后果,并发访问同一些热点 key 的缓存,导致并发崇建 key ,多余且高压,可能严重到临时关停服务器。
缓存击穿有这些解决方案,互斥锁还有逻辑过期两种。 互斥锁的机制是每个线程在查询缓存时未命中后获取互斥锁成功后由该线程开始重建缓存,这期间其他线程获取互斥锁会失败,缓存重建完成的最后再释放锁。互斥锁的优点有没有额外的内存消耗、很好地保证了数据一致性,实现起来比较简单,缺点也很明显,互斥锁导致各个并发线程都要等待,性能受影响,可能有死锁风险。 逻辑过期的方式解决缓存击穿,事实上永不过期,但给一个逻辑过期时间。逻辑过期后就会首先返回已经过期但尚未更新的缓存数据,然后异步地例如开一个线程或任务来重建缓存。逻辑过期的优点就是线程无需等待,性能较好。缺点是不保证强一致性,异步会有额外内存消耗。
如何保持 Redis 的数据一致性
大致三种处理策略吧,淘汰缓存策略、先淘汰缓存再更新数据库的策略还有延时双删策略。 策略一,淘汰缓存。数据如果为较为复杂的数据时,进行缓存的更新操作就会变得异常复杂,因此一般推荐选择淘汰缓存,而不是更新缓存。 策略二,先淘汰缓存,再更新数据库。假如先更新数据库,再淘汰缓存,如果淘汰缓存失败,那么后面的请求都会得到脏数据,直至缓存过期。假如先淘汰缓存再更新数据库,如果更新数据库失败,只会产生一次缓存穿透,相比较而言,后者对业务则没有本质上的影响。 策略三,延时双删策略。如下场景:同时有一个请求 A 进行更新操作,另一个请求 B 进行查询操作。请求 A 进行写操作,删除缓存。请求 B 查询发现缓存不存在。请求 B 去数据库查询得到旧值。请求 B 将旧值写入缓存。请求 A 将新值写入数据库。这样,次数便出现了数据不一致问题。采用延时双删策略得以解决。
说说缓存数据更新策略
三大更新策略是什么
三大更新策略是内存淘汰、超时剔除和主动更新。 内存淘汰是不用自己维护的,利用 Redis 的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存。但是它的一致性极差,完全取决于内存对哪些数据的淘汰,完全无法确定哪些请求会获取到缓存中的旧数据数据。不过这样就无维护成本,不用自己维护。 超时剔除就是给缓存数据添加 TTL 时间,到期后自动删除缓存,下次查询时更新缓存。一致性一般,根据定义的缓存数据数据生命周期 TTL 时间决定,时间短一致性就高,时间长一致性就低。 主动更新就是编写业务逻辑,在修改数据库的同时,更新缓存。具有很高的一致性,根据业务逻辑的严密性决定,如果足够严密,可以做到完美。但是有很高的维护成本,完全要根据已有业务逻辑定制。
说说有哪些主动更新策略
主动更新策略又有三种缓存分离模式、读/写直通模式还有写后缓存模式。 缓存分离模式,就是由缓存的调用者,在更新数据库的同时更新缓存。优点是充分的自由度和定制化。但是开发成本很高。缓存的数据不能是更新只能是删除,因为每一次数据库的数据更新,都会导致缓存数据更新,期间没有查询,导致做无用功,而更新数据库时缓存失效删除 ( 即便 n 次更新也之删除 1 次 ) ,查询时再更新缓存,这样性价比高。而且必须是先操作数据库,后删缓存这样更好,因为这样异常的概率极低。如果删除缓存成功但是数据库操作失败,那么缓存中的数据已经被删除,但是数据库中的数据仍然存在,这就造成了数据的不一致性。 读/写直通模式,就是缓存与数据库整合为一个服务,由该服务来维护一致性,调用者调用该服务,无需关心缓存一致性问题。优点是无需关心二者一致性问题。缺点是为自动保证一致性,导致了多次的缓存与数据库的读写 ( 写放大 ) ,而且也会出现一致性问题,并且因为操作封装起来了,导致这个一致性不可控。 写后缓存模式,就是调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。优点是自动持久化,无需关系数据库。缺点就是因为这个自动持久化是将原先多次手动持久化的一个合并,这就导致,当缓存和内存中数据过多时刚好服务器宕机,容易导致。
Redis 内存淘汰机制有哪些
Redis 的内存淘汰策略有这些,LRU 淘汰、LFU 淘汰、TTL 淘汰、Random 淘汰最后还有淘汰前策略。 LRU ( Least Recently Used,即淘汰最近最少使用的 ) 就是 Redis 会从已经设置了过期时间的 key 中挑选最近最少使用的 key 进行淘汰。LRU 算法会记录每个 key 的最近访问时间,当内存不足时,会优先淘汰最近最少被访问的 key。 LFU ( Least Frequently Used,即淘汰最不经常使用的 ) 就是Redis 会从已经设置了过期时间的 key 中挑选最近使用次数最少的 key 进行淘汰。LFU 算法会记录每个 key 被访问的次数,当内存不足时,会优先淘汰被访问次数最少的 key。 TTL ( Time To Live,即根据生存时间淘汰 ) 就是Redis 会优先淘汰已经过期的 key。当 key 设置了过期时间,且过期时间到达时,key 会被标记为过期,但并不会立即删除。当内存不足时,会优先删除已经过期的 key。 Random ( 即随机淘汰 ) 就是Redis 会随机选择一些 key 进行淘汰。这种策略相对简单,但可能导致一些热点数据被删除,影响系统性能。 淘汰前策略 ( Eviction Policy ) 就是 Redis 4.0 引入了新的淘汰策略,允许用户自定义淘汰前策略。用户可以自定义一个 Lua 脚本来决定哪些 key 应该被优先淘汰。
聊聊 Redis 持久化机制
Redis 有两种持久化机制,一是默认的 R-D-B 持久化机制,还有就是A-O-F 持久化机制。 R-D-B 就是 Redis DataBase 即 Redis 作为数据库,是 Redis 默认的持久化方式,具有快照机制,也就是按照一定的时间将内存的数据以快照的形式保存到硬盘中,可通过配置文件中的 save 参数来定义快照的周期,对应产生的数据文件为 dump.rdb。R-D-B 持久化是记录 Redis 数据库的所有键值对。R-D-B 持久化后数据恢复机制的过程是在某个时间点将数据写入一个临时文件持久化结束后,用这个临时文件替换上次持久化的文件达到数据恢复。R-D-B 的优点有持久化方便、容灾性良好,容灾性好,一个文件可以保存到安全的磁盘,不会涉及大面积风险。还有性能较好性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化,使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能。相对于数据集大时,比 A-O-F 的启动效率更高。但是性能安全性低宕机容易丢失数据。 持久化机制之 A-O-F Append Only File 即仅追加文件。它将 Redis 执行的每次写命令记录到单独的日志文件中。A-O-F 优先于 R-D-B。A-O-F 的优点是数据安全,有同步文件,A-O-F 持久化可以配置 appendfsync 属性,有 always 属性, 每进行一次命令操作就记录到 A-O-F 文件中一次。优点还有就是一致性保障通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-A-O-F 工具解决数据一致性问题。优点还有合并崇写多余命令,A-O-F 机制的 rewrite 模式,A-O-F 文件没被 rewrite 之前 ( 文件过大时会对命令 进行合并崇写 ) ,可以删除其中的某些命令 ( 比如误操作的 flushall ) 。A-O-F 的缺点就是恢复速度慢,因为A-O-F 文件比 R-D-B 文件大。启动效率不高,数据集大的时候,比 R-D-B 启动效率低。 A-O-F 有三种刷盘策略 Always 刷盘策略、Everysec 刷盘策略还有 No 刷盘策略。Always 刷盘策略是在每个写命令被执行时,Redis都会将该命令追加到AOF文件中,并通过 fsync() 系统调用将数据强制刷写到磁盘。这是最安全的策略,因为它可以确保数据不会丢失,但是会带来较大的性能损失,因为频繁的磁盘IO会影响Redis的性能。Everysec ( 即 every second 每秒刷盘) 刷盘策略,在每秒钟执行一次fsync操作。Redis会将所有在一秒内发生的写命令缓冲到内存中,然后在每秒钟的时间点上执行一次fsync操作,将缓冲的写命令写入到AOF文件,并刷写到磁盘。这种策略在性能和数据安全之间取得了一定的平衡,因为虽然数据可能会丢失一秒钟,但是相对于always策略,性能损失较小。No 刷盘策略 就是 Redis 不会主动执行 f-sync 操作,而是交由操作系统来处理数据的持久化。这意味着 Redis 将数据写入到 AOF 文件中,但并不会主动将数据刷写到磁盘。这种策略下,操作系统会根据自身的调度策略将数据写入到磁盘,可能会存在较大的数据丢失风险,但性能损失最小。
Linux 和 Git 工具
Linux 的常见命令有哪些
1. pwd
显示当前工作目录的完整路径。
2. cd
更改当前工作目录到指定的路径。
3. ls
列出当前目录下的文件和文件夹。
4. mkdir
创建一个新的目录。
5. rm
删除文件或目录。
6. stat
显示文件或目录的详细信息,包括权限、所有者、大小、修改时间等。
7. cp
复制文件或目录到另一个位置。
8. mv
移动或重命名文件或目录。
9. grep
在文件中搜索匹配指定模式的行,并打印出来。
10. find
在指定目录下搜索满足特定条件的文件和目录。
平时有用过 Git 吗
新增文件的命令。
git add file 或者 git add。
提交文件的命令。
git commit –m 或者 git commit –a。
查看工作区状况。
git status –s。
拉取合并远程分支的操作。
git fetch/git merge 或者 git pull。
查看提交记录命令。
git reflog。
git init。
创建仓库。
git status。
查看仓库的状态。
git diff 文件名。
这次相较上次修改了哪些内容。
git add 文件名。
将添加的文件放到栈存区中。
git commit。
将栈存区内容提交到代码区中。
git clone git 地址。
将远程仓库的代码克隆到本地。
git branch。
查看当前分支。
git checkout。
切换分支。