知识记录【自用】
juc
synchronized
synchronized 底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能或得到锁的。synchronized 属于悲观锁。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
monitor内部维护了三个变量
- WaitSet:保存处于Waiting状态的线程
- EntryList:保存处于Blocked状态的线程
- Owner:持有锁的线程
Java并发编程三大特性【一句话说明 线程安全】
- 原子性
- 可见性
- 有序性
GC
G1 回收过程
Mixed GC:
- 初始标记 - STW,标记 GC-Roots 直接关联的对象
- 并行标记,标记所有可达的对象
- 重新标记 - STW,标记前两步中变化的对象
- 估计 Region 价值
- 清除 - STW
对于 Young GC 来说,回收全部新生代 Region 即可,不需要对 Region 进行价值估计。
G1 在 CMS 的基础上做的优化
CMS 有哪些缺点:
- 基于连续内存布局,只能回收整个老年代,无法预测停顿时间
- 标记-清除算法,会留下内存碎片
G1 做的改进:
基于 Region,弱化分代,会跟踪 Region 回收价值从而优先选择垃圾最多、收益最大的 Region,从而预测回收时间。
局部标记 - 复制 + 宏观的标记 - 整理,达到减少内存碎片的效果。
mysql
UPDATE记录锁的工作机制
- 锁的类型:当UPDATE语句通过索引条件定位到需要更新的行时,InnoDB会为这些行加上排他锁(X锁)。排他锁会阻止其他事务修改这些行,也会阻止其他事务在这些行上加共享锁或排他锁。
- 锁的范围:如果UPDATE语句使用了唯一索引(包括主键)且条件精确匹配一行,则只锁定这一行。如果UPDATE语句使用了非唯一索引,则可能锁定多行,并且可能还会锁定间隙(使用间隙锁)以防止幻读。如果UPDATE语句没有使用索引,则可能会锁住整个表(在InnoDB中,如果没有使用索引,MySQL会进行全表扫描,并锁住所有扫描到的行,同时还会在间隙上加锁)。
- InnoDB的行锁是基于索引实现的,所以如果更新语句使用了索引(包括主键索引和非主键索引),那么它通常会锁定符合条件的索引记录 
Bean生命周期:
1. 实例化 (创建对象) → 2. 依赖注入 (@Autowired) → 3. 初始化 → 4. 使用 → 5. 销毁
反射的实现原理
反射机制主要涉及以下类:
- java.lang.Class:代表一个类或接口。
- java.lang.reflect.Field:代表类的字段。
- java.lang.reflect.Method:代表类的方法。
当使用Class.forName()或类加载器加载一个类时,JVM会执行以下步骤:
- 加载:通过类的全限定名获取类的二进制字节流,将类的静态存储结构转化为方法区的运行时数据结构,并生成一个代表该类的Class对象。
- 链接:将类的二进制数据合并到JVM运行状态中,包括验证、准备和解析(可选)。
- 初始化:执行类构造器<clinit>()方法,初始化静态变量和静态代码块。
反射API通过访问这个Class对象来获取类的元数据,并可以动态创建对象、调用方法、访问字段等。
反射的优缺点
优点
- 动态性:可以在运行时动态获取类的信息,动态创建对象和调用方法,使得程序更加灵活。
- 通用性:可以编写通用的代码,例如框架中经常使用反射来加载不同的类,实现解耦。
缺点
- 性能开销:反射操作比直接调用慢,因为需要解析类信息,以及方法调用时需要检查访问权限等。虽然现代JVM对反射进行了优化,但仍然有一定的性能损失。
- 安全性限制:反射可以突破访问权限的限制,例如可以调用私有方法和访问私有字段,这可能会破坏封装性,导致安全问题。
- 内部暴露:反射可以访问类的私有成员,这可能会破坏类的封装,并且可能随着版本更新,内部实现改变而导致反射代码失效
Spring事件驱动:发布、传输、监听都是怎么实现的?
Spring框架中的事件驱动模型基于观察者模式,主要包含以下几个角色:
- 事件(ApplicationEvent):所有事件都必须继承自ApplicationEvent类,通常用于封装事件相关的数据。
- 事件发布者(ApplicationEventPublisher):通过ApplicationEventPublisher的publishEvent方法来发布事件。
- 事件监听者(ApplicationListener):实现ApplicationListener接口,或者使用@EventListener注解来标记事件处理方法。
实现过程:
定义事件:class OrderCreateEvent extends ApplicationEvent {发布事件的步骤:1.定义事件类:创建一个事件类,继承自 ApplicationEvent,该类封装了你要传递的事件数据
- 发布事件:通过applicationContext.publishEvent(new OrderCreateEvent(tradeOrder));//发布事件。当事件发布后,Spring会将事件传递给所有匹配的监听器。
- 传输:Spring的事件传输是在同一个JVM内进行的,默认是同步的。也就是说,事件发布后,会直接调用监听器的处理方法。注意,如果有一个监听器处理事件时抛出异常,可能会影响其他监听器的执行以及事件发布者的后续执行。
- 监听:监听器可以通过实现ApplicationListener接口,并指定事件类型,或者使用@EventListener注解来监听特定的事件。当事件发布时,Spring会通过反射调用匹配的监听器。
此外,Spring还支持异步事件处理,可以通过在监听器方法上使用@Async注解,并配置Spring的异步任务执行器来实现
oom
os
线程切换和进程切换的上下文都存什么?
线程切换上下文:线程是CPU调度的基本单位。线程切换时,需要保存当前线程的上下文,以便之后能恢复执行。上下文主要包括:程序计数器(PC):当前线程执行到的指令地址。
寄存器状态:包括通用寄存器、栈指针等。
线程状态:如运行状态、阻塞状态等。
栈信息:每个线程有自己的栈,用于存储局部变量、方法调用等。
注意:线程切换发生在同一进程内,因此虚拟内存空间不变,所以不需要切换页表等内存管理相关的信息。
进程切换上下文:进程是资源分配的基本单位。进程切换时,除了需要保存线程切换所需的上下文外,还需要切换内存空间、文件描述符、信号处理等进程相关的资源。具体包括:
内存管理信息:页表、内存映射等(因为不同进程有独立的虚拟地址空间)。
文件描述符表:进程打开的文件和I/O状态。
信号处理设置:信号掩码、信号处理函数等。
进程控制块(PCB)中的其他信息:如进程ID、优先级、资源使用情况等。
进程切换比线程切换开销大,因为进程切换需要切换内存地址空间,导致TLB失效,从而可能增加内存访问的开销。
Redis
- 为了解决redis节点变化导致的⼤规模数据迁移问题,⼀致性哈希分区出现了:它将整个哈希值空间想象成⼀个环,节点
和数据都映射到这个环上。数据被分配到顺时针⽅向上遇到的第⼀个节点。因为Redis集群槽的个数刚好是 2 的 14 次⽅,和 HashMap 中数组的⻓度必须是 2 的幂次⽅有着异曲同⼯之妙。它能保证扩容后,⼤部分数据停留在扩容前的位置,只有少部分数据需要迁移到新的槽上。如果下线的是主节点,它的从节点之⼀将被选举为新的主节点,接管原主节点负责的哈希槽。
2. Redis字典,更底层是⽤数组+链表实现的哈希表。它的设计很巧妙,⽤了两个哈希表,平时⽤第⼀个,rehash 的时候⽤第⼆个,这样可以渐进式地进⾏扩容,不会阻塞太久。【当负载因⼦触发 rehash 条件时,Redis 会为哈希表1 分配新的空间,通常是哈希表 0 的两倍⼤⼩,然后将rehashidx 设置为 0。接下来的关键是,Redis 不会⼀次性把所有数据从哈希表0 迁移到哈希表1,⽽是每次操作字典时,顺便迁移哈希表0 中 rehashidx 位置上的所有键值对。迁移完⼀个槽位后,rehashidx 递增,直到整个哈希表0 迁移完毕。在 rehash 期间,查找操作会先查 哈希表 0,没找到再查哈希表 1;但是新插⼊的数据只会放到哈希表 1 中。这样既可以保证数据的完整性,⼜能避免数据的重复。这种设计的巧妙之处在于把 rehash 的开销分摊到了每次操作中。假设有⼀个⼏百万键的哈希表,如果⼀次性rehash 可能需要⼏百毫秒,这对单线程的 Redis 来说是灾难性的。但通过渐进式 rehash,每次操作只增加很少的额外开销,⽤户基本感觉不到延迟。】
5.压缩列表是 Redis 为了节省内存⽽设计的⼀种紧凑型数据结构,它会把所有数据连续存储在⼀块内存当中。 整个结构包含头部信息,如总的字节数、尾部偏移量、节点数量,以及连续的节点数据。当 list、hash 和 set 的数据量较⼩且值都不⼤时,底层会使⽤压缩列表来实现。
3.压缩列表 ziplist,这个设计很有意思。Redis 为了节省内存,设计了这种紧凑型的数据结构,把所有元素连续存储在⼀块内存⾥。但是它有个致命问题叫"连锁更新",就是当我们修改⼀个元素的时候,可能会导致后⾯所有的元素都要重新编码,性能会急剧下降。
你知道为什么Redis 7.0要⽤listpack来替代ziplist吗?listpack紧凑列表 它让每个节点只记录⾃⼰的⻓度信息,不再依赖前⼀个节点的⻓度。这样就从根本上避免了连锁更新的问题。
4. zset当保存的元素数量少于 128 个,且保存的所有元素⼤⼩都⼩于 64 字节时,Redis 会采⽤压缩列表的编码⽅式;否则就⽤跳表
分布式
针对你关心的Kafka消息路由、Rebalance机制以及Redis动态字符串扩容问题,我梳理了它们的核心原理,具体如下:
🚀 Kafka的消息路由与分区算法
当Producer发送消息时,确定消息分配到哪个Partition(分区)的规则(也称为分区器)主要有以下三种方式:
| 指定目标分区 | 在代码中直接指定消息的目标分区。 | 
| Key哈希取模 | 若消息设置了Key,则对Key计算哈希值,然后与分区总数取模,得出目标分区。这可保证同一Key的消息总进入同一分区。 | 
| 轮询调度 | 若未指定分区也未设Key,默认采用轮询 方式依次将消息发送到各个分区,以实现均匀分布。 | 
你可以通过实现
Partitioner接口来自定义分区策略。
🔄 Kafka的Rebalance机制
Rebalance(再平衡)是指当消费者组内的消费者成员发生增减(如新consumer加入、旧consumer掉线)时,为了保持负载均衡,整个消费者组会重新分配其订阅Topic的所有分区的过程。
Rebalance的核心过程主要包含以下步骤:
- 消费者注册:新加入或发生变化的消费者会向Kafka集群注册,汇报自身情况。
- 分区重分配:Kafka集群根据新的消费者组成员列表,按照组内配置的分区分配策略,计算出新的分区分配方案。
- 分区转移与消费恢复:新的消费者开始消费分配到的分区,而不再拥有分区所有权的旧消费者则停止消费。
常见的分区分配策略包括:
- RangeAssignor:按分区范围顺序分配给消费者。简单但可能导致分区数量多的Topic在消费者间分配不均。
- RoundRobinAssignor:使用轮询方式分配所有分区,追求绝对均匀。
- StickyAssignor:在均衡分配的前提下,尽可能保留上一次分配中已有的分区所有权,以减少因分区转移带来的资源开销。
- CooperativeStickyAssignor:StickyAssignor的增强版,通过多次小规模重平衡来实现更平滑的调整。
💎 总结
- Kafka消息路由:让你能通过Key或轮询等方式控制消息流向特定分区。
- Kafka Rebalance:是Kafka保证消费者组高可用和伸缩性的核心机制,在消费者变动时自动重新分配分区。
 查看10道真题和解析
查看10道真题和解析