面试题总结
java 集合
Collection
存放单一元素
List
- 有序可重复
ArrayList
:Object
数组,不同步,线程不安全- 内部基于动态数组实现,比
Array
使用起来更灵活 - 可以添加
null
值,但是不建议,会难以维护判空异常
- 内部基于动态数组实现,比
Vector
:古早的以Object
数组为基础的LinkedList
:双向链表(1.6 之前是循环链表,1.7 取消了循环),不同步,线程不安全- 因为是由链表构成的,所以不支持随机访问,不能实现
RandomAccess
接口
- 因为是由链表构成的,所以不支持随机访问,不能实现
Set
- 无序不可重复
HashSet
:基于HashMap
实现- 检查重复:对象加入
HashSet
时,会计算hashCode
值来判断对象的加入位置,同时也会与其他加入的对象的hashCode
的值比较,如果没有相符的,则会认为没有重复;否则,会调用equals()
方法来检查hashCode
相等的对象是否真的相同
- 检查重复:对象加入
LinkedHashSet
:HashSet
的子类,通过LinkedHashLMap
实现TreeMap
:红黑树实现
Queue
- 按照特定顺序排序,有序可重复
Comparable
:出自java.lang
包,有一个compareTo(Object obj)
方法Comparator
:出自java.util
包,有一个compare(Object obj1, Object obj2)
方法
Map
以键值对形式存储
-
HashMap
:1.8 之前以数组和链表组成,链表是为了解决哈希冲突存在;之后改成了数组/链表+红黑树(当链表长度超过阈值 8 时,先判断数组是否超过 64,超过,将链表转换为红黑树,没超过,先把数组扩容),线程不安全-
因为线程安全问题,效率高于
HashTable
-
可以存储
Null key
和Null value
,但是null
键只有一个,null
值可以有多个 -
默认初始化大小为 16,每次扩容容量变为原来 2 倍,如果给了初始值,则每次扩容容量为 2 的幂次方大小
-
- 第一个线程:在头节点插入前被调度挂起
- 第二个线程:因为使用的是头插法,原来的头节点插入新链表后,第二个节点头插法插入同一个数组下标下的链表,线程结束
- 第一个线程:把新数组下标中存储第二个线程的更新后的那个头节点,此时尾节点的后继指向了头节点
-
1.8 后,多个线程更新同一个桶的数据会产生数据覆盖的风险
-
-
LinkedHashMap
:继承自HashMap
,底层是基于拉链式散列结构即数组和链表或红黑树组成,另外多加了双向链表,可以头插和尾插数据,线程不安全 -
HashTable
:数组+链表,链表主要为了解决哈希冲突,线程安全(因为内部方法都是用synchronized
修饰的)- 不允许存储
Null key
和Null value
- 不允许存储
-
TreeMap
:红黑树,线程不安全- 实现了
NavigableMap
接口:有了对集合内元素搜索的能力 - 实现了
SortedMap
接口:有了对集合中元素根据键排序的能力,默认是按Key
的升序排序
- 实现了
遍历方式
Concurrenthashmap 是怎么实现线程安全的(具体读写)
- 1.7 之前主要依赖于分段锁 -
Segment
- 由多个
Segment
组成(默认为 16),每个Segment
维护一个HashEntery
数组(数组+链表,类似与 1.7 之前的HashMap
) - 每个
Segment
继承ReetrantLock
,用于实现分段锁,每个锁只锁容器其中一部分数据,多线程访问不会存在锁竞争,提高并发访问率- 因为继承了
ReetrantLock
,所以Segment
是一种可重入锁,扮演锁的角色
- 因为继承了
- 读操作
- 如
get(key)
不会加锁,直接通过volatile
变量保证可见性
- 如
- 写操作
put
、remove
需要锁住单个Segment
,并不会影响其他Segment
- 计算
key
的hash
值,找到Segment
,然后使用Segment
的ReentrantLock.lock()
进行加锁操作 - 插入或删除数据后释放锁
- 由多个
- 1.8 后彻底移除了
Segment
,改用数组+链表+红黑树的数据结构,并使用CAS + Synchronized
实现线程安全- 由
Node<K, V>[] table
作为主数据存储 - 当表长度超过一定阈值(8)时,转换为红黑树
- 锁粒度更细,
synchronized
只锁定当前链表或红黑二叉树的首节点,只要不产生hash
冲突就不会产生并发 - 读操作
get(key)
操作不会加锁:- 计算
key
的hash
值,找到bucket
(数组槽) - 直接读取
table[index]
,如果是链表或红黑树,则遍历查找 - 由于
Node
的value
使用volatile
修饰,所以可以保 证可见性,无需加锁
- 计算
- 写操作:
put(key, value)
:- 计算
key
的hash
值,找到数组的index
位置 CAS
方法创建新Node
:如果该bucket
为空,则用CAS
插入新节点,避免加锁Synchronized
锁定链表或红黑树- 如果
CAS
失败,说明该bucket
已有数据,则用synchronized
直接锁定该桶的头节点,进行插入 - 如果是链表,遍历后插入
- 如果是红黑树,按照红黑树规则插入
- 如果
- 计算
- 由
红黑树的优势
红黑树是一种自平衡二叉搜索树( BFS
)
- 优势
- 保持平衡,最坏情况下时间复杂度
O(logn)
- 普通的二叉搜索树(
BFS
)在极端情况下会退化成链表(即高度接近n
),导致O(n)
的查询复杂度 - 红黑树通过旋转和变色操作保持平衡,保证最坏情况下的增删改查操作都维持在
O(logn)
,不会出现严重的退化
- 普通的二叉搜索树(
- 插入、删除效率高
- 插入、删除操作的调整成本较低:自平衡调整(变色 + 最多 2 次旋转)开销较小
- 适用于高并发
- 在
ConcurrentHashMap
中,当桶中的链表长度超过 8 时,转换成红黑树,提高查询性能 - 插入、删除的调整操作相对较少,在并发环境下更稳定,不会频繁进行全书重平衡
- 在
- 空间占用较低
- 红黑树不需要存储额外的平衡因子
- 只需要额外存储 1 位颜色信息(红/黑)
- 保持平衡,最坏情况下时间复杂度
jvm 虚拟机
数据库的隔离级别
定义了多个事务并发执行时的数据可见性,主要用于控制脏读、不可重复读、幻读等问题
- 读未提交:最低的隔离级别,事务可以读取其他未提交事务的数据,并发性高,但数据一致性最差
- 脏读:事务 A 读取了事务 B 未提交的数据,如果 B 回滚, A 读到的数据就是无效的
- 不可重复读:事务 A 多次读取同一行数据,期间事务 B 修改并提交,导致 A 读取的值不一致
- 幻读:事务 A 读取了某个范围的数据,事务 B 插入了新数据,导致 A 重新读取时看到了幻影数据
- 读已提交:只能读取已经提交的数据,避免了脏读,比读未提交安全,仍支持较高的并发性
- 不可重复读、幻读
- 可重复读:事务执行期间,读取的所有数据保持一致,防止不可重复读。
- 幻读
Mysql InnoDB
通过MVCC
(多版本并发控制)避免了幻读
- 可串行化:最高级别,每个事务必须依次执行,完全避免并发问题
- 可能导致死锁
- 需要表级锁或行级锁,影响系统吞吐量
数据库索引优化措施
volatile
主要用于保证变量的可见性和防止指令重排,但不保证原子性,每次使用都要到主内存中读取
-
当一个变量被
volatile
修饰后,所有线程都能看见它的最新值:JVM 通过内存屏障和禁止CPU
缓存优化,确保变量的修改立即对所有线程可见 -
volatile
会禁止CPU
和编译器对代码进行指令重排,保证代码按预期执行顺序运行:通过插入特定的内存屏障来禁止指令重排,例如:双重校验锁实现对象单例(线程安全)-
为变量
uniqueInstance
分配内存空间 -
初始化
uniqueInstance
-
将
uniqueInstace
指向分配的内存地址
但是由于
JVM
具有指令重排的特性,执行顺序可能变成 1-> 3 -> 2。指令在单线程的情况下不会出现问题,但是多线程环境下会导致一个线程获得还没有初始化的实例 -
-
不能保证原子性:因为当多个线程同时读取被
volatile
修饰的变量时,可能同时读取到同一个值,而不是依次读取- 可以利用
synchronized
、Lock
、AtomicInteger
来实现正确操作
- 可以利用
synchronized
用于保证代码块或方法在多线程环境下的同步执行,以确保原子性、可见性、有序性
- 保证原子性:确保多个线程同时访问共享资源时,操作不会被中断
- 保证可见性:线程对变量的修改对其他线程立即可见(因为同步代码块执行时,会刷新
CPU
缓存) - 保证有序性:防止指令重排,确保代码按预期执行
用法
-
修饰实例方法:锁当前对象
-
synchronized void method() { // 业务代码 }
-
-
修饰静态方法:锁当前类,会作用于类的所有对象实例,进入同步代码前要获取当前
class
的锁-
synchronized static void method() { // 业务代码 }
静态
synchronized
方法和非静态synchronized
方法之间的调用不互斥:因为访问静态方法占用的锁是当前类的锁,二访问非静态方法占用的锁是当前实例对象锁
-
-
修饰代码块:锁指定对象/类,对括号里指定的对象/类加锁
synchronized(Object)
表示进入同步代码库前要获得给指定对象的锁synchronized(类.class)
表示进入同步代码前要获得给指定Class
的锁
-
构造方法不能用
synchronized
修饰,但是可以在方法内部去修饰代码块
底层原理
synchronized
底层是依靠JVM
进入/退出对象的Monitor
(监视器)实现的- 获取对象的
Monitor
:- 若
Monitor
空闲,则线程占用Monitor
,执行代码 - 若
Monitor
被占用,线程进入阻塞状态,直到获取Monitor
- 若
- 释放
Monitor
:- 代码执行完毕,或抛出异常,线程释放
Monitor
,其他线程才能进入
- 代码执行完毕,或抛出异常,线程释放
- 获取对象的
ReentrantLock
是 Java
并发包( java.util.concurrent.locks
)提供的一种可重入且独占式的锁,相较于 synchronized
,它提供了更灵活的锁管理:
-
可重入锁:也叫递归锁,同一线程可多次获取同一把锁
- 在
A
线程中开启锁后调用B
线程(也开启锁),不会阻塞。因为同一线程能多次lock()
,但要匹配unlock()
的次数
- 在
-
支持公平锁/非公平锁
- 公平锁:锁被释放后,先申请的线程先得到锁。性能较差,上下文切换更频繁
- 非公平锁:锁被释放后,后申请的线程可能会先获取锁,是随机或者按照其他优先级排序。性能更好,但可能导致某些线程饿死
-
支持可中断锁:
lockInterruptibly()
允许线程在等待锁时被中断,抛出异常(Thread.interrupt()
) -
支持超时获取锁:提供了
tryLock(time, unit)
的方法,尝试在指定时间内获取锁,避免死锁,超时后返回false
,可以执行其他逻辑,而不会永远阻塞 -
支持多个条件变量:
Condition
接口- 在一个
Lock
对象中可以创建多个Condition
实例(即对象监视器),线程对象可以注册在指定的Condition
中,从而可以有选择的进行线程通知,在调度线程上更加灵活。 - 在使用
notify()/notifyAll()
方法进行通知时,被通知的线程是由JVM
选择的,用ReentrantLock
类结合Condition
实例可以实现“选择性通知”
- 在一个
synchronized
vs ReentrantLock
对比 | synchronized | ReentrantLock |
---|---|---|
可重入 | ✅ 支持 | ✅ 支持 |
公平锁 | ❌ 默认非公平 | ✅ 可公平/非公平 |
阻塞中断 | ❌ 不支持 | ✅ lockInterruptibly() 支持中断 |
超时获取 | ❌ 不支持 | ✅ tryLock(time, unit) 可超时 |
性能 | 🚀 低竞争时优化 | 🚀 更灵活 |
自动装配(Autowiring)
是 Spring
依赖注入(DI
)的一种方式,它可以让 Spring
自动根据类型、名称等,找到合适的 Bean
并注入,而无需手动配置
自动装配方式 | 说明 |
---|---|
no (默认) | 不使用自动装配,需手动 @Bean 或 XML 配置 |
byName | 根据 属性名 匹配同名 Bean |
byType | 根据 类型 匹配 Bean |
constructor | 根据 构造方法参数 自动装配 |
autodetect (过时) | 先尝试 constructor ,如果失败,则 byType |
自动配置主要由@EnableAutoConfiguration
实现,添加了@EnableAutoConfiguration
注解,会导入AutoConfigurationImportSelector
类,里面的selectImports
方法通过SpringFactoriesLoader.loadFactoryNames()
扫描所有含有META-INF/spring.factories
的jar
包,将对应key
为@EnableAutoConfiguration
注解全名对应的value
类全部装配到IOC
容器中。
设计模式应用及好处
解决特定问题的最佳实践,能够提高代码的可维护性、复用性和扩展性
设计模式分为三大类:
类型 | 作用 | 常见模式 |
---|---|---|
创建型模式 | 解决对象创建问题,隐藏对象创建逻辑 | 单例、工厂、抽象工厂、建造者、原型 |
结构型模式 | 解决类与类之间的关系,提高复用性 | 代理、装饰、适配器、桥接、组合、外观、享元 |
行为型模式 | 解决对象间通信,提高灵活性 | 观察者、策略、命令、责任链、模板方法、状态、迭代器、备忘录、解释器 |
创建型模式
- 单例模式:确保一个类只有一个实例,并提供一个全局访问点
- 工厂模式:在不暴露创建对象的逻辑的前提下,使用工厂方法来创建对象
- 抽象工厂模式:提供一个接口,用于创建相关或依赖对象的实例,而不需要指定实际实现类
- 建造者模式:将复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示
- 原型模式:通过克隆来创建对象,避免了通过
new
关键字显示调用构造函数的开销
构造型模式
- 适配器模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而无法一起工作的类可以一起工作
- 桥接模式:将抽象部分与它的实现部分分离,以便它们可以独立的变化
- 组合模式:将对象组合成树形结构以表示部分-整体的层次结构,使得客户端使用单个对象或组合对象具有一致性
- 装饰器模式:动态地给一个对象添加一些额外的职责,就增加功能而言,装饰器模式比生成子类方式更为灵活
- 外观模式:为子系统中的一组接口提供一个一致的界面,使得子系统更容易使用
- 享元模式:运用共享技术来有效地支持大量细粒度对象的复用
- 代理模式:为其他对象提供一种代理以控制对这个对象的访问
行为型模式
- 责任链模式:为解除请求的发送者和接收者之间的耦合,而将请求的处理对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止
- 命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作
- 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子
- 迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示
- 中介者模式:用一个中介对象封装一系列的对象交互,使得这些对象不需要显示地互相引用,从而降低耦合度
- 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
- 观察者模式:定义对象间的一种一对多的依赖关系,当前对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新
- 状态模式:允许一个对象在其内部状态发生改变时改变其行为,对象看起来似乎改变了它的类
- 策略模式:定义一系列的算法,将每个算法封装起来,并使它 们之间可以互换
- 模板方法模式:定义一个操作中的算法骨架,将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤
- 过滤器设计模式:允许在不改变原始对象的情况下,动态地添加或删除对象的行为
设计模式六大原则
- 单一职责原则:一个类应该只有一个引起它变化的原因。一个类应该只有一项职责,这样可以保证类的内聚性,并且降低类之间的耦合
- 开闭原则:一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。当需要添加新功能时,应尽量通过扩展已有代码来实现,而不是修改已有代码。
- 里氏替换原则:子类应该能够替换父类并且不影响程序的正确性。这意味着在使用继承时,子类不能修改父类已有的行为,而只能扩展父类的功能
- 接口隔离原则:客户端不应该依赖于它不需要的接口。一个类应该只提供它需要的接口,而不应该强迫客户端依赖于它不需要的接口
- 依赖倒置原则:高层模块不应该依赖于底层模块,都应该依赖于抽象。抽象不应该依赖于具体实现,而具体实现应该依赖于抽象
- 迪米特法则:一个对象应该对其他对象保持最少的了解。一个对象只应该与它直接作用的对象发生交互,而不应该与其他任何对象发生直接交互。可以降低类之间的耦合性,提高系统的灵活性和可维护性
组合使用
- 工厂模式 + 单例模式:使用工厂模式创建对象,通过单例模式来保证该工厂只有一个实例,从而减少创建对象时的开销
- 模板方法模式 + 策略模式:使用模板方法模式来定义算法的骨架,同时使用策略模式来定义算法的不同实现方式,以实现更高的灵活性
- 策略模式 + 工厂模式:使用工厂模式创建不同的策略对象,然后使用策略模式来选择不同的策略,以实现不同的功能
- 适配器模式 + 装饰器模式:适配器模式用于一个接口转换成另一个接口,而装饰器模式则用于动态地给对象添加一些额外的职责。
- 观察者模式 + 命令模式:观察者模式用于观察对象的状态变化,并及时通知观察者。而命令模式则用于将一个请求封装成一个对象,可以在运行时动态地切换命令的接收者。当我们需要观察对象的状态变化,并在状态变化时执行一些命令时,可以使用
好处
✅ 提高代码复用性,减少重复代码 ✅ 降低代码耦合度,使模块独立 ✅ 增强扩展性,新增功能时修改更少 ✅ 符合 SOLID 设计原则,提高软件质量
如何处理异常
@RestController 和@Controller 的区别
@RestController
- 适用于
RESTful API
- 继承自
@Controller
,并自动包含@ResponseBody
- 方法返回值默认是
JSON
或XML
(基于Jackson
或Gson
进行序列化) - 不需要手动加
@ResponseBody
@Controller
-
适用于返回视图(如
Thymeleaf
,JSP
) -
需要手动使用
@ResponseBody
才能返回JSON
-
默认返回的是一个视图(
HTML
页面),而不是JSON
如果在方法上加上
@RequestBody
则该API
不会返回视图,而是返回JSON
数据
java 里的 json
常用的 JSON
解释库包括
-
Jackson
(Spring
默认推荐)// 对象 转 json
String json = objectMapper.writeValueAsString(user);
// json 转 对象
User userObj = objectMapper.readValue(json, User.class);
// json 转 map
Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {}); -
Gson
(Google
提供)// 对象 转 json
String json = gson.toJson(new User("Bob", 30));
// json 转 对象
User user = gson.fromJson(json, User.class); -
FastJSON
(阿里巴巴提供,性能较优)// 对象 转 json
String json = JSON.toJSONString(new User("Charlie", 28));
// json 转 对象
User user = JSON.parseObject(json, User.class); -
org.json
(官方JSON
解析库)import org.json.JSONObject;
JSONObject jsonObject = new JSONObject("{\"name\":\"David\",\"age\":40}");
System.out.println(jsonObject.getString("name")); // David
JSON 库 | 适用场景 | 优势 | 劣势 |
---|---|---|---|
Jackson | Spring Boot 默认 | 功能强大,支持 POJO 直接转换 | 依赖较多 |
Gson | 轻量级 JSON 解析 | API 简单易用 | 速度稍慢 |
FastJSON | 高性能 JSON 解析 | 解析速度快 | 安全性争议 |
org.json | 轻量级官方库 | 纯粹 JSON 处理 | 功能较少 |
正向、反向代理
正向代理(服务端看不见客户端)
客户端通过代理服务器访问目标服务器。客户端先向代理服务器发送请求,代理服务器在代表客户端去访问目标服务器,并将结果返回给客户端。
流程:
- 客户端请求访问目标服务器,但无法直接访问
- 请求先发送到正向代理服务器
- 代理服务器转发请求到目标服务器,获取响应数据
- 代理服务器将数据返回给客户端
反向代理(客户端看不见服务端)
客户端不能直接访问目标服务器,而是通过代理服务器访问,代理服务器再去请求目标服务器,并将结果返回给客户端
用于:
- 负载均衡
- 隐藏真实服务器
- 安全防护(防 DDoS )
- 缓存加速
流程:
- 客户端向代理服务器(反向代理)发送请求
- 反向代理服务器将请求转发给后端真实服务器
- 真实服务器返回响应数据给反向代理服务器
- 反向代理服务器将数据返回给客户端
== 和 equals 的区别
对于字符串变量来说,使用 ""
和 "equals"
比较字符串时,其比较方法不同。""
比较两个变量本身的值,即两个对象在内存中的首地址,"equals"
比较字符串包含内容是否相同
对于非字符串变量来说,如果没有对 equals()
进行重写的话,"=="
和 "equals"
方法的作用是相同的,都是用来比较队形在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象
==
:比较的是两个字符串内存地址的数值是否相等,属于数值比较equals
:比较的是两个字符串的内容,属于内容比较
java final 关键字作用
-
修饰类:当
final
修饰类时,表示这个类不能被继承,是类继承体系中的最终形态。如,String
类就是用final
修饰,保证了String
类的不可变性和安全性 -
修饰方法:用
final
修饰的方法不能在子类中被重写。 -
修饰变量:当
final
修饰基本数据类型的变量时,该变量一旦被赋值就不能再更改。对于引用数据类型,final
修饰意味着这个引用变量不能再指向其他对象,但是对象本身的内容可以改变。
Spring IOC 的实现原理
IOC:控制反转,是一种设计思想。在传统的 Java SE
程序设计中,我们直接在对象内部通过 new
的方式来创建对象,是程序主动创建依赖对象;而在 Spring
程序中,IOC
是有专门的容器去控制对象。
所谓控制就是对象的创建、初始化、销毁:
-
创建对象:原来是
new
一个,现在是Spring
容器创建 -
初始化对象:原来是对象自己通过构造器或
setter
方法给依赖的对象赋值,现在由Spring
容器自动注入 -
销毁对象:原来是直接给对象复制
null
或做一些销毁操作,现在是Spring
容器管理生命周期负责销毁对象
所谓反转:是反转控制权,我们由对象的控制者变成了 IOC
的被动控制者
深拷贝和浅拷贝的区别
-
浅拷贝:只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。
-
深拷贝:在复制对象的同时,将对象的所有引用类型字段的内容也复制一份,而不是共享引用。
mybatis 中 # 和 $ 符号的区别
-
Mybatis
在处理#{}
时,会创建预编译的SQL
语句,将SQL
中的#{}
替换为?
号,在执行SQL
时会为预编译SQL
中的占位符(?)
赋值,调用PreparedStatement
的set
方法来赋值,预编译的SQL
语句执行效率高,并且可以防止SQL
注入,提供更高的安全性,适合传递参数 -
Mybatis
在处理${}
时,只是创建普通的SQL
语句,然后在执行SQL
语句时Mybatis
将参数直接拼入到SQL
里,不能防止SQL
注入,因为参数直接拼接到SQL
语句中,如果参数未经过验证、过滤、可能会导致安全问题
maven 中 install 和 package 区别
-
package
:此命令会对项目进行编译、测试,然后把项目打包成特定格式的文件,像JAR
、WAR
等 -
install
:该命令不但会完成package
的操作,还会把打包好的文件安装到本地的Maven
仓库里,方便其他项目引用
maven 怎么管理不同的版本号
-
直接指定版本号
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>...</version>
</dependency>
</dependencies> -
使用属性管理版本号
<properties>
<junit.version>4.13.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies> -
依赖范围管理
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
线程并发的场景里保证线程安全
-
synchronized
:可以使用synchronize
关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问。对象锁是通过synchronized
关键字锁定对象的监视器来实现 -
volatile
:volatile
关键字用于变量,确保所有线程看到的是改变量的最新值,而不是可能存储在本地寄存器的副本 -
Lock
接口和ReentrantLock
类:java.util.concurrent.locks.Lock
接口提供了比synchronized
更强的锁锁定机制,ReentrantLock
实现该接口,提供了更灵活的锁管理和更高的性能 -
原子类:
Java
并发库提供了原子类,如AtomicInteger
、AtomicLong
等,这些类提供了原子操作,可以用于更新基本类型而无需额外的同步 -
线程局部变量:
ThreadLocal
类可以为每个线程提供独立的副本,每个线程都有自己的变量 -
并发集合、
JUC
工具类
SpringAOP
用于实现切面编程
功能:
-
核心业务:登录、注册、
crud
-
周边功能:日志、事务管理
在面向切面编程中,核心业务功能和周边功能是分别独立进行开发,二者不是耦合的然后把切面功能和核心业务功能组合,叫 AOP
AOP
便于减少系统的重复代码、降低模块间的耦合度,有利于未来的可拓展性和可维护性
-
AspectJ
:切面,没有具体的接口或类与之对应,是Join point
,Advice
和Pointcut
的一个统称 -
Join point
:连接点,指程序执行过程中的一个点,例如方法调用、异常处理等。 -
Advice
:通知,即定义一个切面中的横切逻辑,有around
,before
,after
三种类型。在很多的AOP
实现框架中,Advice
通常作为一个拦截器,也可以包含多个拦截器作为一条链路围绕着连接点进行处理 -
Pointcut
:切点,用于匹配连接点,一个切面中包含哪些Join point
需要由Pointcut
进行筛选 -
Introduction
:引介,让一个切面可以声明被通知的对象实现任何他们没有真正实现的额外的接口。 -
Weaving
:织入,在有了连接点、切点、通知以及切面,通过织入,在切点的引导下,将通知逻辑插入到目标方法上,使得我们的通知逻辑在方法调用时得以执行 -
AOP proxy
:AOP
代理,指在AOP
实现框架中实现切面协议的对象。 -
Target object
:目标对象(被代理的对象)
悲观锁和乐观锁的区别
-
乐观锁:乐观锁认为竞争不总是发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,应有相应的重试逻辑
-
悲观锁:认为竞争总是发生,每次对某资源进行操作时,都会持有一个独占锁,如
synchronized
响应式编程
响应式编程是一款使用异步数据流编程的响应式编程思想,提供了非阻塞、异步的特性,便于处理异步场景,从而避免回调地狱和突破 Future
的局限性
响应式编程可以理解为:当某一主题发生改变时,观察此主题的观察者就会立刻收到通知并做出一系列响应
单例模式的应用场景
单例模式可以确保一个类只有一个实例,并提供一个全局访问点来获取这个实例
如:在应用程序里,日志记录器负责将程序运行过程中的信息记录下来,这些信息可能会被写入文件、数据库或者发送到远程服务器。为了保证日志信息的一致性和避免资源竞争,通常会用单 例模式。确保整个应用程序只使用一个实例,避免数据错误或资源浪费
工厂模式
核心思想是将对象的创建和使用分离,把对象创建逻辑封装在一个工厂类中,这样在使用对象时,无需关心对象的具体创建过程,只需向工厂请求获取所需对象即可
优势:让代码的可维护性和可扩展性更强,可以根据不同的需求创建不同类型的对象,当需要添加新的对象类型时,只需修改或扩展工厂类
-
简单工厂模式:工厂模式的基础版本,定义了一个工厂类,该类包括一个创建对象的方法,根据传入的参数来决定创建哪种类型的对象
// 汽车接口
interface Car {
void drive();
}
// 轿车类
class Sedan implements Car {
@Override
public void drive() {
System.out.println("Driving a sedan.");
}
}
// SUV 类
class SUV implements Car {
@Override
public void drive() {
System.out.println("Driving an SUV.");
}
}
// 汽车工厂类
class CarFactory {
public static Car createCar(String type) {
if ("sedan".equalsIgnoreCase(type)) {
returnnew Sedan();
} elseif ("suv".equalsIgnoreCase(type)) {
returnnew SUV();
}
returnnull;
}
}
// 测试代码
publicclass SimpleFactoryExample {
public static void main(String[] args) {
Car sedan = CarFactory.createCar("sedan");
sedan.drive();
Car suv = CarFactory.createCar("suv");
suv.drive();
}
} -
工厂方法模式:将对象的创建逻辑延迟到子类中实现,每个具体的子类负责创建特定类型的对象。定义了一个抽象的工厂类,其中包含一个抽象的创建对象的方法,具体的创建逻辑由子类实现
// 汽车接口
interface Car {
void drive();
}
// 轿车类
class Sedan implements Car {
@Override
public void drive() {
System.out.println("Driving a sedan.");
}
}
// SUV 类
class SUV implements Car {
@Override
public void drive() {
System.out.println("Driving an SUV.");
}
}
// 抽象工厂类
abstract class CarFactory {
public abstract Car createCar();
}
// 轿车工厂类
class SedanFactory extends CarFactory {
@Override
public Car createCar() {
return new Sedan();
}
}
// SUV 工厂类
class SUVFactory extends CarFactory {
@Override
public Car createCar() {
return new SUV();
}
}
// 测试代码
public class FactoryMethodExample {
public static void main(String[] args) {
CarFactory sedanFactory = new SedanFactory();
Car sedan = sedanFactory.createCar();
sedan.drive();
CarFactory suvFactory = new SUVFactory();
Car suv = suvFactory.createCar();
suv.drive();
}
} -
抽象工厂模式:提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类 。允许客户端使用抽象的接口来创建一组相关的对象,而不需要关心这些对象的具体实现。
// 汽车接口
interface Car {
void drive();
}
// 轮胎接口
interface Tire {
void roll();
}
// 宝马汽车类
class BMWCar implements Car {
@Override
public void drive() {
System.out.println("Driving a BMW car.");
}
}
// 宝马轮胎类
class BMWTire implements Tire {
@Override
public void roll() {
System.out.println("BMW tire is rolling.");
}
}
// 奔驰汽车类
class BenzCar implements Car {
@Override
public void drive() {
System.out.println("Driving a Benz car.");
}
}
// 奔驰轮胎类
class BenzTire implements Tire {
@Override
public void roll() {
System.out.println("Benz tire is rolling.");
}
}
// 抽象工厂接口
interface CarFactory {
Car createCar();
Tire createTire();
}
// 宝马工厂类
class BMWFactory implements CarFactory {
@Override
public Car createCar() {
returnnew BMWCar();
}
@Override
public Tire createTire() {
returnnew BMWTire();
}
}
// 奔驰工厂类
class BenzFactory implements CarFactory {
@Override
public Car createCar() {
returnnew BenzCar();
}
@Override
public Tire createTire() {
returnnew BenzTire();
}
}
// 测试代码
publicclass AbstractFactoryExample {
public static void main(String[] args) {
CarFactory bmwFactory = new BMWFactory();
Car bmwCar = bmwFactory.createCar();
Tire bmwTire = bmwFactory.createTire();
bmwCar.drive();
bmwTire.roll();
CarFactory benzFactory = new BenzFactory();
Car benzCar = benzFactory.createCar();
Tire benzTire = benzFactory.createTire();
benzCar.drive();
benzTire.roll();
}
}
哈希算法和加密算法的区别
-
哈希算法:散列算法,把任意长度的输入数据通过特定的哈希函数转换为固定的长度输出,这个输出值就是哈希值,也称为摘要。哈希函数是确定性的,相同的输入始终会产生相同的输出,并且计算过程是单向的,很难从哈希值反推原始输入数据。常见的哈希算法 -
MD5
、SHA-1
、SHA-256
等 -
加密算法:是将明文数据经过特定的加密密钥和加密算法转换为密文,在需要的时候,再使用对应的解密密钥和解密算法将密文还原为明文。加密过程是可逆的,前提是拥有正确的密钥。加密算法分为对称加密算法和非对称加密算法。
-
对称加密算法:
DES
、AES
,加密和解密使用相同的密钥 -
非对称加密算法:
RSA
、ECC
,使用公钥加密,私钥解密
kafka 的选举机制
Kafka
的选举包括:Kafka Controller
的选举,Partition Leader
的选举, Consumer Group Coordinator
(消费组协调者)的选举
Kafka Controller
的选举过程:
-
启动竞争:集群启动时,各个
Broker
节点会尝试在Zookeeper
中创建一个临时节点/controller
。ZooKeeper
是一个分布式协调服务,可保障只有一个节点能成功创建该节点。 -
确定控制器:成功创建
/controller
节点的Broker
会成为控制器,并在该节点中记录自身的Broker ID
等信息 -
故障处理:若控制器所在节点发生故障,其创建的临时节点会自动消失。其他
Broker
监听到这一变化后,会重新竞争创建/controller
节点,从而选出新的控制器
Partition Leader
的选举:
-
控制器主导:分区首领选举由控制器负责。控制器会维护分区的状态信息,当检测到首领副本不可用时,会触发选举流程
-
优先副本选择:
Kafka
优先选择分区的优先副本作为首领。优先副本是在分区创建时指定的第一个副本 -
存活副本选择:若优先副本不可用,控制器会从同步副本集合(ISR)中选择一个副本作为新的首领。同步副本是指定与首领副本保持同步的副本
-
元数据更新:选举出新首领后,控制器会更新集群元数据,通知其他
Broker
新的首领信息
Consumer Group Coordinator
(消费组协调者)的选择:
-
协调器确定:消费者组中的消费者在启动时,会向
Kafka
集群发送请求,Kafka
根据消费者组ID
的哈希值选择一个Broker
作为协调器 -
注册与通知:消费者向选定的协调器注册自己,协调器会维护消费者组的成员信息,并将分区分配方案通知给各个消费者。
-
故障处理:若协调器所在的
Broker
故障,消费者会收到通知,重新发起协调器选举
Redis 的持久化机制
Redis
的读写操作都是在内存中,所以 Redis
性能才会高,但是当 Redis
重启后,内存中的数据就会丢失,为了保证内存中的数据不会丢失,Redis
实现了数据持久化的机制。这个机制会把数据存储到磁盘,这样在 Redis
重启后就能从磁盘中恢复原有数据。
-
AOF
日志:每执行一条写操作命令,就把该命令以追加的 方式写入一个文件里-
Redis
重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复 -
Redis
提供了 3 种写回硬盘的策略:-
Always
:每次写操作命令执行完后,同步将AOF
日志数据写回硬盘 -
Everysec
:每次写操作命令执行完后,先将命令写入到AOF
文件的内核缓冲区,然后每隔一秒将缓冲区数据写回硬盘 -
No
:不由Redis
控制写回时机,转交给操作系统控制写回时机,每次写操作命令执行完后,先将命令写入到AOF
文件的内核缓冲区,再有操作系统决定何时将缓冲区内容写回
-
-
-
RDB
快照:将某一时刻的内存数据,以二进制的方式写入磁盘。提供了两个命令来生成RDB
文件-
save
:会在主线程生成RDB
文件,由于和执行操作命令在同一个线程,所以如果写入RDB
文件时间太长,会阻塞主线程 -
bgsave
:会创建一个子进程来生成文件,可避免主线程阻塞
-
AOF 和 RDB 优缺点
AOF:
-
优点:首先,
AOF
提供了更好的数据安全性,因为它默认每接收到一个写命令就会追加到文件末尾。即使Redis
服务器宕机,也只会丢失最后一次写入前的数据。其次,AOF
支持多种同步策略(如everysec
、always
等),可以根据需要调整数据安全性和性能之间的平衡。同时,AOF
文件在 Redis 启动时可以通过重写机制优化,减少文件体积,加快恢复速度。并且,即使文件发生损坏,AOF
还提供了redis-check-aof
工具来修复损坏的文件。 -
缺点:因为记录了每一个写操作,所以
AOF
文件通常比RDB
文件更大,消耗更多的磁盘空间。并且,频繁的磁盘IO
操作(尤其是同步策略设置为always
时)可能会对Redis
的写入性能造成一定影响。而且,当某个文件体积过大时,AOF
会进行重写操作,AOF
如果没有开启AOF
重写或者重写频率较低,恢复过程可能较慢,因为它需要重放所有的操作命令。
RDB:
-
优点:
RDB
通过快照的形式保存某一时刻的数据状态,文件体积小,备份和恢复的速度非常快。并且,RDB
是在主线程之外通过fork
子进程来进行的,不会阻塞服务器处理命令请求,对Redis
服务的性能影响较小。最后,由于是定期快照,RDB
文件通常比AOF
文件小得多。 -
缺点:
RDB
方式在两次快照之间,如果Redis
服务器发生故障,这段时间的数据将会丢失。并且,如果在RDB
创建快照到恢复期间有写操作,恢复后的数据可能与故障前的数据不完全一致
布隆过滤器
布隆过滤器由「初始值都为 0 的位图数组」和「 N
个哈希函数」两部分组成。 当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
布隆过滤器会通过 3 个操作完成标记:
- 使用
N
个哈希函数分别对数据做哈希计算,得到N
个哈希值; - 将第一步得到的
N
个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。 - 将每个哈希值在位图数组的对应位置的值设置为 1;
查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据
CAP 理论
CAP
原则又称 CAP
定理, 指的是在一个分布式系统中, Consistency
(一致性)、 Availability
(可用性)、Partition tolerance
(分区容错性), 三者不可得兼
-
一致性(C) : 在分布式系统中的所有数据备份, 在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)
-
可用性(A): 在集群中一部分节点故障后, 集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)
-
分区容忍性(P): 以实际效果而言, 分区相当于对通信的时限要求. 系统如果不能在时限内达成数据一致性, 就意味着发生了分区的情况, 必须就当前操作在 C 和 A 之间做出选择
索引的建立怎么考虑
什么时候适用索引?
-
字段有唯一性限制的,比如商品编码;
-
经常用于
WHERE
查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。 -
经常用于
GROUP BY
和ORDER BY
的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在B+Tree
中的记录都是排序好的。
什么时候不需要创建索引?
-
WHERE
条件,GROUP BY
,ORDER BY
里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。 -
字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为
MySQL
还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。 -
表数据太少的时候,不需要创建索引; 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于需要维护索引的有序性,有额外的维护成本。
MySQL 页大小
MySQL
的页( Page
)大小通常是 16KB( InnoDB
引擎默认),但可以通过 innodb_page_size
参数调整为 4KB、8KB
或 16KB
。页是 MySQL
物理存储的基本单位,所有的数据、索引等都存储在页中。