上海回合肥年初面试第六天😐

67 阅读51分钟

1.什么情况用乐观锁,什么情况用悲观锁

乐观锁适用场景​

  • ​读多写少​​:事务间以读取为主,冲突概率低(如博客阅读量统计)。

  • ​高并发写入但冲突较少​​:例如用户积分更新(多数操作不会冲突)。

  • ​长事务场景​​:事务执行时间长,但冲突概率低(如订单状态流转)。

  • ​实现方式​​:

    • 数据库版本号(version 字段)或时间戳。
    • CAS(Compare And Swap)操作(如 Java 的 AtomicInteger)。

典型场景举例​​:

  • 电商系统中的商品库存扣减(若库存冲突少,可用乐观锁)。
  • 社交媒体的点赞数更新(冲突概率低)。

悲观锁适用场景​

  • ​写多读少​​:事务间频繁修改同一数据(如银行转账)。

  • ​高冲突场景​​:数据竞争激烈(如限量秒杀库存)。

  • ​强一致性要求​​:必须保证数据操作的原子性,不允许脏读或覆盖。

  • ​实现方式​​:

    • 数据库行锁(SELECT ... FOR UPDATE)。
    • 分布式锁(如 Redis 的 SETNX 或 RedLock)。

​典型场景举例​​:

  • 支付系统的账户余额扣减(需严格避免超扣)。
  • 库存秒杀场景(高并发下需强制排他)。

乐观锁​​:适合冲突少、追求高并发的场景,通过版本号或CAS实现,简单高效。
​悲观锁​​:适合冲突多、强一致性的场景,通过排他锁避免竞争,但需谨慎处理死锁。

2.redis事务

Redis事务通过一组命令实现​​批量操作的原子性执行​​,但其事务模型与传统关系型数据库(如MySQL)有显著差异

  1. ​MULTI​

    • 标记事务的开始,后续命令会被放入队列,直到执行EXEC

    • ​示例​​:

      MULTI
      SET key1 "value1"
      INCR key2
      EXEC
      
  2. EXEC​

    • 执行事务队列中的所有命令,按顺序串行化执行。
    • ​特性​​:即使某条命令报错(如类型错误),其他命令仍会执行,​​不支持回滚​​。
  3. ​DISCARD​

    • 放弃事务队列中的所有命令,清空事务。
  4. ​WATCH key [key ...]​

    • ​乐观锁机制​​:监视一个或多个键,若在事务执行前这些键被修改,事务会被取消(EXEC返回nil)。
    • ​用途​​:实现基本的并发控制,避免数据竞争。

Redis事务的特性​

​特性​​说明​
​原子性​事务内的命令会按顺序​​串行执行​​,不会被其他客户端命令打断。
​错误处理​- ​​语法错误​​:若命令存在语法错误(如参数错误),整个事务不执行。
- ​​运行时错误​​:如对字符串类型执行INCR,仅该命令失败,其他命令继续执行。
​不支持回滚​Redis事务没有回滚机制,即使命令执行失败,事务仍会继续执行后续命令。
​轻量级​事务的本质是命令队列,无锁机制,性能开销低。
特性​​Redis事务​​关系型数据库事务(如MySQL)​
​原子性​所有命令按顺序执行,但部分失败不影响整体所有操作要么全部成功,要么全部回滚
​隔离性​无隔离级别,依赖单线程模型保证串行化支持多种隔离级别(如读未提交、可重复读)
​回滚​不支持支持
​适用场景​批量操作、简单并发控制复杂事务、强一致性需求

Redis事务通过MULTI/EXEC实现​​批量命令的原子化执行​​,适合简单操作和轻量级并发控制,但​​不支持回滚​​和复杂隔离级别。对于强一致性需求或复杂逻辑,建议使用​​Lua脚本​​替代事务。理解其特性与局限性,可避免误用场景(如金融交易)。

3.redis定时消息队列

​​Redisson 的 RDelayedQueue​​

当我们要添加一个数据到延迟队列的时候,redission会把数据+超时时间放到zset中,并且起一个延时任务,当任务到期的时候,再去zset中把数据取出来,返回给客户端使用。

我们用RDelayedQueue的offer方法将订单添加到了延迟队列,并指定了延迟时间,当元素的延迟时间到达时,Redission会将元素从RDelayedQueue转移到与之关联的RBlockingDeque。

然后在检查是否要关单的时候,另起了一个线程,不断循环读取到期的订单。值得注意的是 take方法从RBlockingDeque中获取元素,这是一个阻塞操作,如果没有元素,它会一直等到,直到有元素。

4.g1底层原理

G1垃圾回收器(Garbage-First)的底层原理​

G1(Garbage-First)是Java HotSpot虚拟机中面向服务端应用的垃圾回收器,设计目标是在​​可控的停顿时间​​和​​高吞吐量​​之间取得平衡。其核心思想是通过​​分区管理​​和​​并发标记​​,优先回收垃圾比例高的区域(Region),从而优化内存管理效率。以下是其底层原理的详细解析:


​一、核心设计思想​

  1. ​分代与分区结合​

    • ​分代模型​​:保留年轻代(Young Generation)和老年代(Old Generation)的概念,但物理上不再连续。

    • ​分区(Region)管理​​:

      • 堆内存被划分为多个大小相等的独立区域(Region),默认大小为 ​​2MB~32MB​​(可配置)。
      • 每个Region可以是 ​​Eden区、Survivor区、Old区或Humongous区​​(存放超大对象)。
      • ​优势​​:动态调整Region用途,减少内存碎片。
  2. ​并发标记与优先回收​

    • ​并发标记阶段​​:与应用程序线程并行,标记存活对象并统计各Region的垃圾比例。
    • ​混合回收(Mixed GC)​​:在回收年轻代的同时,选择部分老年代Region(垃圾比例高的)进行回收,避免Full GC。

​二、关键数据结构与技术​

  1. ​Region与内存布局​

    • ​Region大小​​:默认由堆总大小自动计算(2MB~32MB),需为2的幂次方。

    • ​Region类型​​:

      • ​Eden​​:新对象分配区域。
      • ​Survivor​​:存活对象迁移区域(From/To)。
      • ​Old​​:长期存活对象存储区域。
      • ​Humongous​​:存放超过Region 50%大小的对象,避免跨Region复制。
  2. ​Remembered Set(RSet)​

    • ​作用​​:每个Region维护一个RSet,记录其他Region引用本Region中对象的指针。
    • ​优势​​:年轻代回收时无需扫描整个老年代,仅需检查RSet中的引用,提升效率。
  3. ​SATB(Snapshot-At-The-Beginning)算法​

    • ​目的​​:在并发标记阶段记录初始对象快照,确保标记的准确性。
    • ​实现​​:通过写屏障(Write Barrier)捕获对象引用变化,记录新增或修改的引用。

​三、工作流程​

  1. ​年轻代回收(Minor GC)​

    • ​触发条件​​:Eden区满时触发。

    • ​步骤​​:

      1. ​初始标记(STW)​​:标记GC Roots直接关联的对象。
      2. ​根区域扫描​​:扫描Survivor区引用的对象。
      3. ​并发标记​​:遍历整个堆,标记存活对象。
      4. ​最终标记(STW)​​:处理并发标记阶段的变更。
      5. ​清理​​:统计各Region存活对象比例,优先回收垃圾比例高的Region。
  2. ​并发标记周期​

    • ​全局并发标记​​:全堆扫描,确定各Region的垃圾比例。
    • ​混合回收(Mixed GC)​​:回收年轻代Region + 部分老年代Region(基于垃圾比例)。
  3. ​Humongous对象处理​

    • 超大对象直接分配到Humongous区,只能通过Full GC回收,需合理设置Region大小以减少此类对象的影响。

​四、核心优势与性能优化​

  1. ​可预测的停顿时间​

    • 通过 -XX:MaxGCPauseMillis 参数设定目标最大停顿时间(默认200ms),G1动态调整每次回收的Region数量以满足目标。
  2. ​高吞吐量​

    • 并发标记与应用程序线程并行,减少停顿对业务的影响。
  3. ​内存碎片控制​

    • 分区管理机制避免传统分代模型中的内存碎片问题。

​五、与其他垃圾回收器的对比​

​特性​​G1​​CMS​​Parallel GC​
​目标​低停顿 + 高吞吐低停顿高吞吐
​内存布局​分区(Region)连续分代空间连续分代空间
​碎片处理​较少(Region机制)易产生碎片无碎片
​停顿预测​支持不支持不支持
​适用场景​大内存、低延迟中小内存、低延迟高吞吐、后台计算

​六、调优参数与实践建议​

  1. ​关键参数​

    • -XX:+UseG1GC:启用G1。
    • -XX:MaxGCPauseMillis:目标最大停顿时间(默认200ms)。
    • -XX:G1HeapRegionSize:Region大小(默认自动计算)。
    • -XX:InitiatingHeapOccupancyPercent:触发并发标记的老年代占用阈值(默认45%)。
  2. ​调优建议​

    • ​堆大小​​:根据应用需求调整,避免频繁Full GC。
    • ​Region大小​​:根据对象分布选择(大对象多则增大Region)。
    • ​监控指标​​:关注 gc pausesyoung gc count 和 mixed gc count

​七、总结​

G1通过​​分区管理​​、​​并发标记​​和​​混合回收​​机制,在保证可预测停顿时间的同时,兼顾高吞吐量。其核心优势在于:

  • ​动态调整​​:根据垃圾比例优先回收高价值区域。
  • ​高效内存利用​​:减少碎片,支持超大堆内存。
  • ​低延迟​​:通过SATB和RSet优化标记与回收效率。

​适用场景​​:大内存(如几十GB)、对延迟敏感的应用(如Web服务、实时系统)。理解其原理有助于合理配置参数,避免性能瓶颈。

5.AtomicInteger底层原理

AtomicInteger 是 Java 提供的一种原子操作类,位于 java.util.concurrent.atomic 包中,主要用于提供在多线程环境下对整数值进行原子更新的操作。它通过底层的硬件支持(如 CAS 操作)来确保线程安全,而不需要使用传统的锁机制(如 synchronizedReentrantLock)。

1. AtomicInteger 的基本作用

AtomicInteger 提供了对整数的原子操作,它的主要方法包括:

  • get():获取当前值。
  • set(int newValue):设置新值。
  • getAndSet(int newValue):获取当前值并设置为新值。
  • incrementAndGet():将当前值加 1 后返回新值。
  • addAndGet(int):将当前值加上指定增量后返回新值。
  • compareAndSet(int expect, int update):如果当前值等于预期值,则将其设置为新的值,返回操作是否成功。

这些方法保证了在多线程环境下对 AtomicInteger 的操作是线程安全的,避免了传统的同步锁机制所带来的性能开销。

2. AtomicInteger 底层原理

AtomicInteger 的底层实现主要依赖于 CAS (Compare-And-Swap) 操作,这是硬件原语提供的一种原子操作,用于实现无锁并发控制。CAS 操作的本质是:

  • 比较内存中的值是否等于预期值。
  • 如果相等,则更新为新的值。
  • 如果不等,则不做任何修改,并返回失败信息。

CAS 操作是原子性的,它通过底层的硬件指令直接支持,通常是 CPU 中的 CMPXCHG 指令,能够保证操作的原子性。

2.1 CAS 操作的流程

AtomicInteger 中的值实际上是通过 volatile 变量来存储的。例如,在 AtomicInteger 的内部,有一个 volatile int value 变量来存储实际的整数值。CAS 操作会使用该 value 变量来进行比较和交换,操作流程如下:

  1. 读取当前值:获取 value 当前值。
  2. 比较当前值和期望值:如果当前值和期望值相同,则可以安全地更新为新的值。
  3. 更新值:通过 CAS 操作原子地将 value 从期望值更新为新的值。
  4. 失败重试:如果 CAS 操作失败(即当前值与期望值不相等),说明其他线程已经修改了 value,需要重新读取并尝试更新,直到成功。

2.2 CAS 操作的实现

AtomicInteger 中的核心操作通常是通过 sun.misc.Unsafe 类来实现的。Unsafe 是 Java 中一个较为底层的类,提供了操作内存和执行 CAS 等低级操作的方法。通过 UnsafeAtomicInteger 可以执行类似 compareAndSwapInt 这样的底层 CAS 操作。

3. 性能优势

相比传统的锁机制,CAS 操作不需要阻塞线程,而是通过不断地尝试更新值来实现线程安全。这种无锁操作大大提高了并发性能,特别是在高并发的情况下,CAS 操作能够减少上下文切换和锁竞争,从而提供更好的性能。

3.1 自旋等待(Spin Wait)

CAS 操作可能会失败,尤其是在高并发的情况下。如果 CAS 操作失败,线程通常会自旋等待,直到获取到更新的权限。这种自旋等待通常不会引入较大的性能开销,特别是当操作失败的次数较少时。

3.2 ABA 问题

CAS 操作存在一个常见的问题,叫做 ABA 问题。即假设一个变量从值 A 变成 B,然后再变回 A,CAS 操作就无法区分该变量是否发生了实际的改变。在多线程高并发的环境中,ABA 问题可能导致错误的判断。为了解决这个问题,Java 提供了 AtomicStampedReferenceAtomicMarkableReference 类,它们引入了版本号或标记,来避免 ABA 问题。

4. 总结

AtomicInteger 是 Java 中提供的一种高效的原子操作类,它通过底层的 CAS 操作来保证线程安全。相比传统的锁机制,AtomicInteger 提供了更高效的并发控制,特别是在高并发环境下。它的底层依赖于硬件支持的原子操作(如 CMPXCHG 指令),并通过 volatile 关键字确保内存可见性。尽管 CAS 操作性能优秀,但它也存在一些问题,如 ABA 问题,Java 通过引入一些其他类来解决这些问题。

6.mysql事务回滚原理

MySQL的事务回滚机制依赖于其存储引擎InnoDB的undo log(撤销日志),通过记录数据修改前的状态来实现回滚操作。以下是其核心原理和流程的详细解析:


​一、事务回滚的核心机制​

​1. Undo Log的作用​

  • ​记录修改前的数据​​:当事务对数据进行修改(INSERT/UPDATE/DELETE)时,InnoDB会将修改前的数据记录到undo log中。
  • ​支持回滚与MVCC​​:undo log不仅用于事务回滚,还为多版本并发控制(MVCC)提供历史数据版本,实现非锁定读。

​2. 回滚的触发场景​

  • ​显式回滚​​:用户执行ROLLBACK命令。
  • ​隐式回滚​​:事务执行过程中遇到错误(如主键冲突、死锁、连接断开等)。

​二、Undo Log的数据结构​

​1. 存储位置​

  • ​独立表空间​​:MySQL 5.6+支持将undo log存储在独立的undo_表空间中(默认共享表空间ibdata1)。
  • ​文件格式​​:每个undo log记录包含表空间ID、页号、事务ID及修改前的数据。

​2. 记录类型​

  • ​INSERT操作​​:记录插入的数据行,回滚时删除对应行。
  • ​DELETE操作​​:记录被删除的数据行,回滚时重新插入。
  • ​UPDATE操作​​:记录更新前的旧值,回滚时恢复旧值。

​三、事务回滚的实现步骤​

  1. ​记录Undo Log​​:
    在事务执行修改操作时,InnoDB会先将原始数据写入undo log,并通过trx_id(事务ID)和roll_ptr(回滚指针)关联到数据行。

  2. ​修改数据​​:
    更新数据页(Buffer Pool中的内存页),并将修改异步刷新到磁盘(通过redo log保证持久性)。

  3. ​回滚操作​​:

    • 当需要回滚时,InnoDB按逆序执行undo log中的操作:

      • ​INSERT​​ → 删除插入的数据。
      • ​DELETE​​ → 重新插入被删数据。
      • ​UPDATE​​ → 用旧值覆盖当前值。
    • 清理undo log:提交的事务对应的undo log会被标记为可清理,由purge线程异步回收。


​四、事务回滚与持久性​

​1. Undo Log的持久化​

  • ​写入redo log​​:undo log的修改会先记录到redo log,确保即使系统崩溃,undo log的内容也不会丢失。
  • ​刷盘策略​​:由innodb_flush_log_at_trx_commit参数控制(默认1,即每次提交时刷盘)。

​2. 回滚的原子性保证​

  • 事务的原子性通过undo log和redo log共同实现:

    • ​undo log​​:提供回滚所需的数据快照。
    • ​redo log​​:保证已提交事务的修改持久化。

​五、事务回滚的性能影响​

​1. 回滚耗时因素​

  • ​事务大小​​:涉及大量数据修改的事务回滚时间更长。
  • ​锁竞争​​:回滚时需持有相关数据行的排他锁,可能阻塞其他事务。

​2. 优化建议​

  • ​缩短事务​​:避免长时间运行的操作,减少事务持有锁的时间。
  • ​批量操作​​:分批次提交事务,降低单次回滚的开销。
  • ​合理设计索引​​:减少全表扫描,提升回滚效率。

​六、与Redo Log的协作​

​日志类型​​作用​​回滚中的角色​
​Redo Log​保证已提交事务的持久性不直接参与回滚,但确保undo log的持久化
​Undo Log​支持回滚和MVCC提供数据修改前的状态,用于逆向恢复

​七、示例场景​

​1. 事务回滚流程​

sql
复制
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 修改前数据写入undo log
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 修改前数据写入undo log
-- 发生错误,执行ROLLBACK
ROLLBACK;
  • ​回滚过程​​:

    1. 根据undo log恢复id=1的balance(+100)。
    2. 根据undo log恢复id=2的balance(-100)。

​2. 提交后的事务​

  • 已提交事务的undo log可能被purge线程清理,无法回滚。

​八、总结​

MySQL的事务回滚通过​​undo log记录数据修改前的状态​​,结合redo log的持久化保证,实现了ACID中的原子性和一致性。其核心流程如下:

  1. ​记录​​:修改数据前写入undo log。
  2. ​修改​​:更新数据页并记录redo log。
  3. ​回滚​​:按逆序应用undo log恢复数据。

​最佳实践​​:

  • 避免大事务,减少回滚风险。
  • 合理配置innodb_undo_logsinnodb_purge_threads以优化性能。
  • 监控trx_rseg_history_len,防止undo log膨胀。

7.spring三级缓存

Spring 的三级缓存机制主要用于解决单例 Bean 的循环依赖问题。其核心是通过分层缓存提前暴露 Bean 的引用,确保 Bean 在未完全初始化时仍能被其他 Bean 依赖注入。以下是三级缓存的详细解析:


​一、三级缓存的结构​

​缓存层级​​作用​​存储内容​
​singletonObjects​存放完全初始化的单例 Bean完整且初始化完成的 Bean 实例
​earlySingletonObjects​存放提前暴露的 Bean(尚未完全初始化)未完成初始化的 Bean 引用
​singletonFactories​存放 Bean 的工厂对象,用于生成早期引用ObjectFactory(生成 Bean 的工厂)

​二、循环依赖的解决流程​

假设有两个 Bean:BeanA 依赖 BeanBBeanB 又依赖 BeanA

  1. ​创建 BeanA​

    • Spring 容器开始创建 BeanA,实例化后生成 ObjectFactory 并放入 singletonFactories
    • 填充 BeanA 的属性时发现依赖 BeanB,于是开始创建 BeanB
  2. ​创建 BeanB​

    • 实例化 BeanB,生成其 ObjectFactory 放入 singletonFactories

    • 填充 BeanB 的属性时发现依赖 BeanA,此时检查 singletonObjects

      • BeanA 不在 singletonObjects(尚未完全初始化)。
      • 检查 earlySingletonObjects,若无则从 singletonFactories 获取 BeanA 的工厂,生成一个 ​​早期引用​​ 并放入 earlySingletonObjects
  3. ​完成 BeanB 的初始化​

    • BeanB 使用早期引用的 BeanA 完成自身初始化,并存入 singletonObjects
  4. ​完成 BeanA 的初始化​

    • BeanA 获取到完全初始化的 BeanB,继续完成自身初始化,并存入 singletonObjects

​三、关键代码逻辑​

java
复制
// 伪代码:创建 Bean 的流程
public Object getSingleton(String beanName) {
    // 1. 检查 singletonObjects(完全初始化的 Bean)
    Object singleton = singletonObjects.get(beanName);
    if (singleton != null) return singleton;

    // 2. 检查 earlySingletonObjects(提前暴露的 Bean)
    singleton = earlySingletonObjects.get(beanName);
    if (singleton != null) return singleton;

    // 3. 从 singletonFactories 获取工厂,生成早期引用
    ObjectFactory<?> factory = singletonFactories.get(beanName);
    if (factory != null) {
        singleton = factory.getObject(); // 生成早期引用
        earlySingletonObjects.put(beanName, singleton);
        singletonFactories.remove(beanName);
        return singleton;
    }

    return null;
}

​四、三级缓存的协作​

  1. ​singletonFactories​​:

    • 在 Bean 实例化后,立即将 ObjectFactory 存入此缓存,用于后续生成早期引用。
    • ​用途​​:允许其他 Bean 在依赖注入时获取未完全初始化的 Bean。
  2. ​earlySingletonObjects​​:

    • 当其他 Bean 请求依赖时,若目标 Bean 正在创建,则从 singletonFactories 生成早期引用并存入此缓存。
    • ​用途​​:缓存早期引用,避免重复生成。
  3. ​singletonObjects​​:

    • Bean 完全初始化后,从 earlySingletonObjects 移动至此缓存,供后续直接使用。

​五、限制与注意事项​

  1. ​仅支持单例 Bean​

    • 原型(Prototype)Bean 不会被缓存,每次请求都生成新实例,无法解决循环依赖。
  2. ​构造方法循环依赖无法解决​

    • 若循环依赖发生在构造方法中(如 BeanA 的构造参数需要 BeanB,反之亦然),三级缓存无法处理,会抛出 BeanCurrentlyInCreationException
  3. ​代理 Bean 的影响​

    • 若 Bean 被 AOP 代理,早期引用可能是代理对象而非原始对象。

​六、总结​

Spring 的三级缓存通过 ​​提前暴露 Bean 引用​​ 解决单例 Bean 的循环依赖问题:

  1. ​singletonFactories​​:生成早期引用的工厂。
  2. ​earlySingletonObjects​​:缓存未完全初始化的 Bean。
  3. ​singletonObjects​​:存放完全初始化的 Bean。

​核心价值​​:

  • 允许 Bean 在未完全初始化时被其他 Bean 依赖注入。
  • 避免因循环依赖导致的死锁或初始化失败。

​最佳实践​​:

  • 尽量避免循环依赖,优先通过 Setter 方法注入。
  • 构造方法依赖需通过 @Lazy 延迟加载解决。
  • 理解其局限性,避免在复杂场景中滥用。

8.spring aop

​Spring AOP 核心原理与实现​

Spring AOP(Aspect-Oriented Programming)是一种基于代理的切面编程框架,用于实现横切关注点(如日志、事务、权限)的模块化。其核心是通过动态代理技术,在不修改原有代码的情况下增强目标对象的行为。以下是其核心原理和关键机制的详细解析:


​一、AOP 核心概念​

​概念​​说明​
​切面(Aspect)​模块化的横切关注点(如日志记录、事务管理),通过注解或XML配置定义。
​连接点(Join Point)​程序执行中的特定点(如方法调用、异常抛出),Spring AOP仅支持方法级别的连接点。
​通知(Advice)​切面在连接点执行的动作,包括前置(@Before)、后置(@After)、环绕(@Around)等。
​切入点(Pointcut)​通过表达式匹配需要增强的连接点(如 execution(* com.example.service.*.*(..)))。
​目标对象(Target)​被切面增强的原始对象。
​代理(Proxy)​Spring AOP 通过动态代理生成的目标对象的增强版本,分为 JDK 动态代理和 CGLIB 代理。

​二、AOP 实现原理​

Spring AOP 的核心是 ​​动态代理​​,其实现方式取决于目标对象是否实现接口:

  1. ​JDK 动态代理​

    • ​条件​​:目标对象实现了接口。

    • ​原理​​:基于 java.lang.reflect.Proxy 生成代理类,代理类实现相同接口,并通过 InvocationHandler 拦截方法调用。

    • ​代码示例​​:

      java
      复制
      public interface UserService {
          void addUser();
      }
      public class UserServiceImpl implements UserService {
          @Override
          public void addUser() { /* ... */ }
      }
      // 代理生成
      UserService proxy = (UserService) Proxy.newProxyInstance(
          UserService.class.getClassLoader(),
          new Class<?>[]{UserService.class},
          (proxyObj, method, args) -> {
              // 前置增强
              System.out.println("Before method");
              // 调用目标方法
              return method.invoke(new UserServiceImpl(), args);
          }
      );
      
  2. ​CGLIB 代理​

    • ​条件​​:目标对象未实现接口。

    • ​原理​​:通过继承目标类生成子类代理,覆盖父类方法并添加增强逻辑。

    • ​依赖​​:需引入 spring-core 中的 CGLIB 库。

    • ​代码示例​​:

      java
      复制
      public class UserService {
          public void addUser() { /* ... */ }
      }
      // CGLIB 代理生成
      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(UserService.class);
      enhancer.setCallback(new MethodInterceptor() {
          @Override
          public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
              // 前置增强
              System.out.println("Before method");
              return proxy.invokeSuper(obj, args);
          }
      });
      UserService proxy = (UserService) enhancer.create();
      

​三、AOP 核心流程​

  1. ​切面定义​​:通过 @Aspect 注解定义切面类,使用 @Pointcut 指定切入点表达式。
  2. ​代理创建​​:Spring 容器在初始化 Bean 时,检查是否需要创建代理(基于切面匹配的 Bean)。
  3. ​方法拦截​​:当调用目标方法时,代理对象拦截请求,按顺序执行通知(Advice)。
  4. ​增强逻辑​​:在连接点前后或环绕执行切面代码(如日志、事务管理)。

​四、通知类型(Advice)​

​通知类型​​执行时机​​代码示例​
@Before方法执行前System.out.println("Before method");
@AfterReturning方法正常返回后记录返回值或清理资源
@AfterThrowing方法抛出异常后记录异常信息
@After方法执行后(无论是否成功)资源释放
@Around包裹目标方法,可控制执行流程支持手动调用 ProceedingJoinPoint

​环绕通知示例​​:

java
复制
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Before method");
    try {
        Object result = joinPoint.proceed(); // 执行目标方法
        System.out.println("After method");
        return result;
    } catch (Exception e) {
        System.out.println("Exception handled");
        throw e;
    }
}

​五、切入点表达式​

通过 @Pointcut 定义切入点,常用语法:

  • ​execution​​:匹配方法签名(最常用)。

    java
    复制
    @Pointcut("execution(* com.example.service.*.*(..))") // 匹配 service 包下所有方法
    public void serviceMethods() {}
    
  • ​within​​:匹配类或包。

    java
    复制
    @Pointcut("within(com.example.service.*)") // 匹配 service 包下所有类
    
  • ​args​​:匹配方法参数类型。

    java
    复制
    @Pointcut("args(String)") // 匹配参数为 String 类型的方法
    

​六、AOP 与事务管理​

Spring 的声明式事务(@Transactional)本质是通过 AOP 实现:

  1. ​切面​​:TransactionAspectSupport 类定义事务切面。
  2. ​通知​​:@Around 通知包裹目标方法,开启、提交或回滚事务。
  3. ​代理​​:目标 Bean 被代理后,事务逻辑透明注入。

​七、AOP 的局限性与适用场景​

​局限性​​说明​
​仅支持方法级别的增强​无法直接拦截字段访问或构造方法。
​基于动态代理​对 final 类或方法无法生成代理(CGLIB 无法继承 final 类)。
​性能开销​频繁的方法拦截可能影响性能(可通过优化切入点表达式减少匹配范围)。

​适用场景​​:

  • 日志记录、性能监控、权限校验。
  • 事务管理、缓存控制。
  • 解耦横切关注点,提升代码复用性。

​八、总结​

Spring AOP 通过 ​​动态代理​​ 和 ​​切面编程模型​​,实现了横切关注点的模块化:

  1. ​核心机制​​:JDK/CGLIB 动态代理生成增强对象。
  2. ​灵活性​​:通过切入点表达式精准匹配目标方法。
  3. ​解耦​​:分离业务逻辑与横切逻辑,提升代码可维护性。

​最佳实践​​:

  • 优先使用 @Around 处理复杂流程(如事务)。
  • 避免过度使用 AOP,防止代码可读性下降。
  • 结合 @Order 注解控制多个切面的执行顺序。

9.spring循环依赖

在 Spring 中,循环依赖(Circular Dependency)指的是两个或多个 Bean 之间相互依赖的情况。例如,Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,这就形成了一个循环依赖问题。

Spring 通过 三级缓存机制(或者说 单例模式下的循环依赖解决方案)来解决这种问题。我们可以理解为:Spring 容器会在实例化 Bean 的过程中,在实例化阶段用一个容器存储 Bean 实例,避免在构造函数阶段出现死锁。通过这种机制,Spring 可以在容器中创建这些依赖并成功注入。

问题描述

假设我们有两个类,AB,其中 A 依赖于 B,而 B 又依赖于 A,如下:

javaCopy Code
@Component
public class A {
    private B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }

    // getter and setter
}

@Component
public class B {
    private A a;

    @Autowired
    public B(A a) {
        this.a = a;
    }

    // getter and setter
}

如果 Spring 容器遇到这种情况,它会抛出 BeanCurrentlyInCreationException 异常,提示存在循环依赖。

Spring 解决循环依赖的原理

Spring 容器会使用 三级缓存机制来解决循环依赖问题,具体机制如下:

  1. 一级缓存(Singleton Objects Cache) :这是 Spring 容器的常规缓存,存放已经创建好的 Bean 实例(最终注入依赖完成的实例)。
  2. 二级缓存(Early Singleton Objects Cache) :当 Spring 开始创建 Bean 实例时,容器会先将正在创建的 Bean(处于中间状态的实例)放入二级缓存。
  3. 三级缓存(Singleton Factory Beans Cache) :用于保存通过工厂方法(ObjectFactory 或 Supplier)生成的 Bean。

Spring 通过这三级缓存来解决循环依赖问题。具体流程是这样的:

  1. Spring 容器在创建 Bean A 时,发现它依赖 Bean B,于是进入 Bean B 的创建过程。
  2. 在创建 Bean B 时,Spring 发现它依赖 Bean A,此时 Bean A 尚未完全创建好,Spring 将 Bean A 放入二级缓存中,并继续创建 Bean B
  3. Bean B 创建完成后,Bean A 继续从二级缓存中获取,Spring 再把 Bean A 填充完整,最后将 Bean A 和 Bean B 完成注入,返回给客户端。

解决循环依赖的关键

通过 构造器注入 的方式存在循环依赖问题,因为 Spring 容器无法在 Bean 实例化之前进行依赖注入。而 Setter 注入字段注入 就不会出现问题,因为 Spring 会在实例化 Bean 后再进行依赖注入操作。

代码示例:解决循环依赖

  1. 构造器注入:  在构造器中直接进行依赖注入,存在循环依赖的风险。
javaCopy Code
@Component
public class A {
    private B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }

    // getter and setter
}

@Component
public class B {
    private A a;

    @Autowired
    public B(A a) {
        this.a = a;
    }

    // getter and setter
}
  1. Setter 注入:  使用 Setter 方法注入依赖,解决了循环依赖问题。
javaCopy Code
@Component
public class A {
    private B b;

    @Autowired
    public void setB(B b) {
        this.b = b;
    }
}

@Component
public class B {
    private A a;

    @Autowired
    public void setA(A a) {
        this.a = a;
    }
}

在 Setter 注入的方式下,Spring 可以先实例化 AB,然后进行依赖注入,而不会出现死锁。

总结

  • 构造器注入:当两个 Bean 之间形成循环依赖时,Spring 容器无法处理,因为容器需要先实例化对象后才能注入依赖,而构造器注入会要求对象完全创建后再注入,容易导致依赖无法满足。
  • Setter 注入:Spring 可以先实例化 Bean,然后进行依赖注入,因此可以解决循环依赖问题。
  • Spring 的三级缓存机制:通过保存正在创建中的 Bean,Spring 解决了循环依赖问题。

注意

  • 循环依赖问题是单例 Bean 的问题,对于 原型 Bean,Spring 是不能解决循环依赖的,因为每次获取原型 Bean 时都会创建新的实例,Spring 不能进行类似的缓存机制。

10.观察者模式

观察者模式(Observer Pattern)是一种常见的设计模式,属于行为型模式。它定义了一种一对多的依赖关系,使得当一个对象的状态发生变化时,所有依赖于它的对象都能得到通知并自动更新。观察者模式适用于事件驱动的系统或用户界面系统中,能够解耦观察者和被观察者之间的关系。

基本概念

  • 被观察者(Subject) :也叫“主题”,是状态发生变化的对象。它维护一个观察者的集合,提供注册、注销观察者的方法。当它的状态发生变化时,它会通知所有注册的观察者。
  • 观察者(Observer) :是依赖于被观察者的对象,它会被通知到状态变化并做出相应的更新操作。
  • 通知机制:当被观察者的状态发生改变时,观察者被通知,通常通过调用观察者的更新方法来实现。

UML类图

在 UML 类图中,观察者模式通常涉及三个主要角色:

  1. Subject:声明一个附加/删除观察者对象的接口。
  2. ConcreteSubject:实现 Subject 接口,维护状态并在发生变化时通知观察者。
  3. Observer:为所有具体的观察者定义一个更新接口。
  4. ConcreteObserver:实现 Observer 接口,接收通知并更新自身的状态。

观察者模式的流程

  1. 观察者注册到被观察者。
  2. 被观察者的状态发生变化。
  3. 被观察者通知所有已注册的观察者。
  4. 观察者收到通知后进行更新。

实例:天气预报

假设有一个天气预报系统,其中有一个 WeatherData 类,它充当被观察者,状态包括温度、湿度、气压等。当这些数据发生变化时,所有订阅该天气数据的观察者(如显示设备、警报系统等)都会被通知并更新自己的显示。

1. 定义观察者接口

javaCopy Code
public interface Observer {
    void update(float temperature, float humidity, float pressure);
}

2. 定义被观察者接口

javaCopy Code
public interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

3. 具体的被观察者类

javaCopy Code
public class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }

    // 模拟气象数据的变化
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObservers();
    }
}

4. 具体的观察者类

javaCopy Code
public class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity.");
    }
}

5. 客户端代码

javaCopy Code
public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
        weatherData.registerObserver(currentDisplay);

        weatherData.setMeasurements(80, 65, 30.4f); // 模拟气象数据变化
        weatherData.setMeasurements(82, 70, 29.2f); // 模拟气象数据变化
    }
}

输出结果

Copy Code
Current conditions: 80.0F degrees and 65.0% humidity.
Current conditions: 82.0F degrees and 70.0% humidity.

优缺点

优点:

  1. 松耦合:观察者模式解耦了被观察者和观察者之间的关系。观察者无需了解被观察者的具体实现,只需要关注状态变化即可。
  2. 扩展性强:添加新的观察者不需要修改现有的被观察者代码,符合开闭原则(对扩展开放,对修改关闭)。
  3. 动态更新:观察者可以动态订阅和取消订阅被观察者,可以根据需要更新。

缺点:

  1. 通知开销:如果观察者很多,通知所有观察者可能会带来性能问题。
  2. 依赖关系:观察者和被观察者之间有一定的依赖关系,虽然这种依赖是解耦的,但大量的观察者订阅可能会增加复杂性。

应用场景

  • 事件处理系统:当事件发生时,需要通知多个监听器或处理器时(如图形用户界面中的按钮点击)。
  • 实时数据更新:如股票市场、天气预报等系统,多个组件需要实时获取状态更新。
  • 发布/订阅系统:消息系统、博客更新等,多个订阅者接收发布者发布的消息。

观察者模式使得系统的模块之间能够低耦合地交互,在动态变化的环境中尤为有用。

11.老年代年轻代

  • 年轻代:用于存储新创建的对象,回收频繁且速度快,回收时使用复制算法,涉及 Eden 区和两个 Survivor 区。回收过程称为 Minor GC。
    频繁回收:年轻代的回收频率较高,因为大多数对象的生命周期较短,很多对象会很快变为垃圾。
    快速回收:年轻代的垃圾回收速度要求较快,以便尽量减少对应用的影响。

  • 老年代:用于存储长时间存活的对象,回收较少且较慢,回收时使用标记-清除或标记-整理算法。回收过程称为 Major GC 或 Full GC。
    较少回收:老年代的垃圾回收不应频繁发生,因为它通常涉及到更多对象,回收时需要的时间较长。
    回收慢:老年代的回收过程通常比年轻代慢,因为它需要处理的对象比较多,并且清理老年代的时间较长。

年轻代与老年代的关系

  • 晋升:当年轻代的对象经过多次垃圾回收后依然存活,它们会被晋升到老年代。这一过程通常通过“晋升阈值”来控制,阈值设定了多少次回收后对象才会晋升。
  • 垃圾回收策略:年轻代的回收采用的是 复制算法,即将存活对象复制到另一区域,而老年代的回收一般采用 标记-清除 或 标记-整理 算法,这意味着它需要清理和整理更多存活的对象。

12.mysql 隔离级别

13.hashmap底层原理

HashMap 的底层原理详解​

HashMap 是 Java 集合框架中最常用的数据结构之一,其核心设计目标是 ​​快速存取键值对​​。它基于哈希表实现,结合了数组和链表(或红黑树)的优势。以下是其底层原理的详细解析:


​一、数据结构​

​1. 底层存储​

  • ​数组 + 链表/红黑树​​:
    HashMap 内部维护一个 Node<K,V>[] table 数组,每个元素是一个链表或红黑树的头节点。

    • ​链表​​:解决哈希冲突的简单方式,遍历链表查找目标节点(时间复杂度 O(n))。

    • ​红黑树​​:当链表长度超过阈值(默认 8)时,链表转换为红黑树,提升查询效率(时间复杂度 O(log n))。

    • ​转换条件​​:

      • 链表长度 ≥ 8 且数组长度 ≥ 64 → 转红黑树。
      • 红黑树节点数 ≤ 6 → 转回链表。

​2. 节点结构​

java
复制
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;       // 哈希值(key.hashCode()的高位混合低位)
    final K key;          // 键
    V value;              // 值
    Node<K,V> next;       // 下一个节点(链表或红黑树)
}

​二、哈希函数​

​1. 计算哈希值​

  • ​目标​​:将键的哈希码均匀分布到数组的索引位置,减少冲突。

  • ​步骤​​:

    1. 调用键的 hashCode() 方法获取原始哈希码。
    2. 通过高位异或低位(扰动函数)进一步分散哈希值,减少碰撞概率。
    3. 与数组长度减一进行按位与运算,得到数组索引。
java
复制
// 扰动函数(Java 8)
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 计算索引
int index = (n - 1) & hash; // n 是数组长度(必须是 2 的幂)

​2. 为什么数组长度是 2 的幂?​

  • 保证 (n - 1) & hash 能均匀覆盖所有索引(0 到 n-1)。
  • 例如,n=16 → n-1=15(二进制 1111),哈希值的低 4 位决定索引。

​三、插入操作(put)​

​1. 插入流程​

  1. ​计算哈希值​​:通过 hash(key) 得到哈希值。

  2. ​定位桶​​:根据哈希值找到数组中的对应位置(桶)。

  3. ​处理冲突​​:

    • ​桶为空​​:直接创建新节点。

    • ​桶不为空​​:

      • 遍历链表或红黑树,若找到相同键(key.equals(node.key)),更新值。
      • 未找到相同键,插入链表尾部或红黑树中。
  4. ​扩容检查​​:插入后若元素数量超过阈值(容量 × 负载因子),触发扩容。

​2. 扩容机制​

  • ​触发条件​​:元素数量 > threshold(默认 容量 × 0.75)。

  • ​扩容方式​​:

    • 新数组大小为原数组的 2 倍。
    • 重新计算所有元素的哈希值,迁移到新数组(rehash)。
  • ​优化​​:

    • 通过 e.hash & oldCap 判断节点在新数组中的位置(原索引或原索引 + 旧容量)。
    • 减少 rehash 的计算量。

​四、查找操作(get)​

  1. ​计算哈希值​​,定位桶。

  2. ​遍历链表或红黑树​​:

    • 链表:逐个比较键(equals())。
    • 红黑树:通过二叉搜索树结构快速查找。
  3. ​返回值​​:找到匹配键则返回值,否则返回 null


​五、线程安全性​

  • ​非线程安全​​:HashMap 不保证多线程环境下的数据一致性。

  • ​并发问题​​:

    • 多线程同时扩容可能导致环形链表(死循环)。
    • 数据覆盖或丢失(如两个线程同时插入不同键)。
  • ​解决方案​​:

    • 使用 Collections.synchronizedMap 包装成同步 Map。
    • 使用 ConcurrentHashMap(分段锁或 CAS + synchronized)。

​六、性能影响因素​

  1. ​初始容量与负载因子​​:

    • 初始容量过小 → 频繁扩容,性能下降。
    • 负载因子过大 → 哈希冲突增多,链表/红黑树变长。
    • ​建议​​:根据预估数据量设置初始容量,负载因子默认 0.75(平衡空间与时间)。
  2. ​哈希函数质量​​:

    • 键的 hashCode() 应尽量分散,减少冲突。
    • 劣质哈希函数会导致性能退化为链表。

​七、Java 8 的优化​

  1. ​红黑树​​:链表过长时转换为红黑树,提升查询效率。
  2. ​尾插法​​:解决头插法在多线程环境下的死循环问题。
  3. ​扰动函数优化​​:通过高位异或低位减少哈希冲突。

​八、与 ConcurrentHashMap 的区别​

​特性​​HashMap​​ConcurrentHashMap​
​线程安全​是(分段锁或 CAS + synchronized)
​锁粒度​无锁锁桶(Java 8)
​扩容机制​单线程扩容多线程协作扩容
​适用场景​单线程或低并发高并发环境

​九、总结​

  • ​核心原理​​:数组 + 链表/红黑树,通过哈希函数快速定位桶,处理冲突时动态调整结构。
  • ​优势​​:平均时间复杂度 O(1)(哈希均匀时)。
  • ​劣势​​:非线程安全,哈希冲突严重时性能下降。
  • ​适用场景​​:缓存、快速查询、非高并发环境。

理解 HashMap 的底层原理,有助于在开发中合理选择数据结构,避免性能瓶颈。

14.concurrenthashmap底层原理

ConcurrentHashMap 是 Java 中线程安全的哈希表实现,其核心设计目标是 ​​高并发下的高性能​​。它在不同 Java 版本中经历了重大改进(尤其是 Java 7 到 Java 8),以下是其底层原理的详细解析:


​一、核心设计思想​

  1. ​分段锁(Java 7)​

    • 将数据分成多个段(Segment),每个段独立加锁,支持多线程并发访问不同段。
    • ​缺点​​:内存开销大,段的数量固定(默认 16),扩容复杂度高。
  2. ​CAS + Synchronized(Java 8+)​

    • 弃用分段锁,改用更细粒度的锁机制:对每个桶(Bucket)的头节点加锁(synchronized),结合 CAS 操作管理元数据。
    • ​优点​​:更高的并发度,减少锁竞争,支持动态扩容。

​二、数据结构(Java 8+)​

​1. 底层存储​

  • ​Node 数组​​:存储键值对的数组,每个元素是一个链表或红黑树的头节点。

    java
    复制
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
        // ...
    }
    
  • ​链表转红黑树​​:当链表长度超过阈值(默认 8)时转为红黑树,提升查询效率(时间复杂度从 O(n) 降到 O(log n))。

​2. 桶数组(Table)​

  • ​懒加载​​:首次插入数据时初始化数组,默认大小为 16。
  • ​动态扩容​​:当元素数量超过阈值(容量 × 负载因子)时,扩容为原数组的 2 倍。

​三、并发控制机制​

​1. CAS 操作​

  • ​用途​​:原子性地更新元数据(如桶数组指针 table、扩容标记 sizeCtl)。

  • ​关键变量 sizeCtl​:

    • -1:表示初始化中。
    • -2:表示扩容中。
    • 其他值:控制并发扩容的线程数(如 -(1 + n) 表示有 n 个线程参与扩容)。

​2. synchronized 锁​

  • ​锁定粒度​​:仅锁定当前操作的桶(Bucket)的头节点,不影响其他桶。
  • ​锁升级​​:当链表转红黑树时,锁的粒度仍为头节点,避免全表锁。

​四、扩容机制​

​1. 触发条件​

  • ​初始化​​:首次插入数据时,若 table 为空,触发初始化。
  • ​扩容​​:元素数量超过阈值(sizeCtl 控制),或迁移旧数组时发现新数组未完成。

​2. 多线程并发迁移​

  • ​ForwardingNode​​:在扩容期间,旧桶的头节点会被替换为 ForwardingNode,新操作会被重定向到新数组。
  • ​数据迁移​​:多线程协作迁移旧数组中的节点到新数组,每个线程负责迁移固定数量的桶(如每次迁移 16 个桶)。

​3. 渐进式扩容​

  • ​双缓冲机制​​:新旧数组共存,逐步迁移数据,避免一次性阻塞所有操作。

​五、读操作的无锁化​

  • ​volatile 变量​​:Node 的 val 和 next 字段均为 volatile,保证可见性。
  • ​无需加锁​​:读操作直接访问 Node 数组,依赖内存屏障保证数据一致性。

​六、关键方法源码解析​

​1. put(K key, V value)

  1. 计算哈希值,定位桶位置。

  2. 若桶为空,通过 CAS 创建新节点。

  3. 若桶不为空,加锁处理:

    • 链表:遍历插入或转红黑树。
    • 红黑树:调用树节点的插入方法。
  4. 检查是否需要扩容。

​2. get(Object key)

  1. 计算哈希值,定位桶位置。
  2. 遍历链表或红黑树,返回匹配的节点值。
  3. 无锁操作,依赖 volatile 保证可见性。

​七、Java 8 vs Java 7 的改进​

​特性​​Java 7(分段锁)​​Java 8(CAS + synchronized)​
​锁粒度​段级锁(默认 16 段)桶级锁(每个桶独立加锁)
​数据结构​数组 + 链表数组 + 链表/红黑树
​扩容机制​分段扩容整体扩容,多线程协作迁移
​并发性能​高吞吐,但内存开销大更高并发度,低内存占用
​锁类型​ReentrantLocksynchronized(JVM 优化)

​八、适用场景​

  1. ​高并发读写​​:如缓存系统、计数器。
  2. ​弱一致性要求​​:迭代器遍历时不阻塞其他操作(弱一致性)。
  3. ​大数据量场景​​:支持动态扩容和高效内存管理。

​九、性能调优建议​

  1. ​初始容量​​:根据预估数据量设置,避免频繁扩容(默认 16)。
  2. ​负载因子​​:默认 0.75,平衡空间与时间效率。
  3. ​并发控制​​:避免热点桶(如大量请求集中在同一哈希值)。

​十、总结​

ConcurrentHashMap 通过 ​​CAS + synchronized 的细粒度锁​​ 和 ​​动态扩容机制​​,实现了高并发下的高性能。其核心优势在于:

  • ​读操作无锁​​:依赖 volatile 和内存屏障。
  • ​写操作低竞争​​:仅锁定单个桶。
  • ​扩容高效​​:多线程协作迁移,渐进式完成。

理解其底层原理有助于在分布式系统、高并发场景中合理使用,避免性能瓶颈。

15.jvm调优

VM 调优的目标是 ​​优化内存使用、减少垃圾回收(GC)停顿时间、提高系统吞吐量​​,同时避免内存溢出(OOM)和性能瓶颈。以下是调优的核心要点和步骤:


​一、JVM 内存结构与关键参数​

​1. 内存区域划分​

​区域​​作用​​常见问题​
​堆(Heap)​存放对象实例OOM、频繁 Full GC
​元空间(Metaspace)​存放类元信息Metaspace OOM
​栈(Stack)​方法调用和局部变量StackOverflowError
​本地方法栈​Native 方法调用内存泄漏
​直接内存​NIO 使用的堆外内存DirectBuffer OOM

​2. 核心调优参数​

​参数​​作用​​推荐值​
-Xmx / -Xms最大/初始堆内存(建议设为相同值)物理内存的 1/4 ~ 1/2
-XX:NewRatio新生代与老年代比例(默认 2:1)2(年轻代占 1/3)
-XX:SurvivorRatioEden 区与 Survivor 区比例(默认 8:1)8
-XX:MetaspaceSize初始元空间大小256M(根据类数量调整)
-XX:MaxMetaspaceSize最大元空间大小512M ~ 1G
-Xss每个线程栈大小1M ~ 2M(高并发场景可减小)

​二、垃圾回收器选择​

​1. 常见 GC 算法对比​

​GC 类型​​适用场景​​特点​
​Serial GC​单线程、小型应用低开销,但 STW 时间长
​Parallel GC​多核 CPU、高吞吐场景多线程并行,适合批处理
​CMS(已废弃)​低延迟应用(如 Web)并发标记清除,碎片化问题
​G1(推荐)​大内存、低延迟场景分区管理,可预测停顿时间
​ZGC/Shenandoah​超大堆(TB 级)、极低延迟亚毫秒级停顿,JDK 11+ 支持

​2. 参数配置示例​

  • ​G1 GC​​(默认 JDK 9+):

    bash
    复制
    -XX:+UseG1GC 
    -XX:MaxGCPauseMillis=200  # 目标最大停顿时间
    -XX:G1HeapRegionSize=32M    # Region 大小(默认自动分配)
    
  • ​ZGC​​(JDK 15+ 生产可用):

    bash
    复制
    -XX:+UseZGC 
    -Xmx16G                     # 支持最大 16TB 堆
    

​三、调优步骤与工具​

​1. 调优步骤​

  1. ​监控与分析​​:

    • 使用 jstatjmapjstack 或 APM 工具(如 Prometheus + Grafana)收集性能数据。
    • 分析 GC 日志(启用 -Xloggc:<file> 和 -XX:+PrintGCDetails)。
  2. ​定位瓶颈​​:

    • 频繁 Full GC → 检查老年代内存是否不足或内存泄漏。
    • 长暂停时间 → 优化 GC 算法或调整堆分区。
    • CPU 过高 → 检查 GC 线程竞争或应用逻辑问题。
  3. ​参数调整​​:

    • 逐步调整参数,验证效果(如 -Xmx-XX:MaxGCPauseMillis)。
  4. ​验证与迭代​​:

    • 压力测试(如 JMeter)验证调优效果,持续监控。

​2. 关键工具​

​工具​​用途​
jstat -gcutil实时监控 GC 状态
jmap -heap查看堆内存分布
jstack分析线程阻塞和死锁
​VisualVM​图形化监控(CPU、内存、线程)
​MAT(Memory Analyzer)​分析堆转储文件,查找内存泄漏

​四、常见问题与解决方案​

​1. OOM(内存溢出)​

  • ​现象​​:java.lang.OutOfMemoryError: Java heap space 或 Metaspace

  • ​原因​​:

    • 堆内存不足(对象过多或内存泄漏)。
    • 元空间加载过多类(动态生成类,如反射、CGLIB)。
  • ​解决​​:

    • 增大 -Xmx 或 -XX:MaxMetaspaceSize
    • 使用 MAT 分析堆转储,排查内存泄漏。

​2. 频繁 Full GC​

  • ​现象​​:GC 日志中频繁出现 Full GC (System.gc())

  • ​原因​​:

    • 老年代空间不足(对象过早晋升)。
    • 显式调用 System.gc()(可通过 -XX:+DisableExplicitGC 禁用)。
  • ​解决​​:

    • 增大老年代大小(调整 -XX:NewRatio)。
    • 优化对象生命周期,减少短命大对象。

​3. CPU 过高​

  • ​现象​​:GC 线程占用大量 CPU。

  • ​原因​​:

    • GC 频繁触发(如堆内存过小)。
    • 应用代码存在死循环或计算密集型操作。
  • ​解决​​:

    • 优化 GC 参数(如增大 -Xmx 或调整 GC 算法)。
    • 使用性能分析工具(如 async-profiler)定位代码热点。

​五、高级调优技巧​

  1. ​逃逸分析​​:

    • 启用 -XX:+DoEscapeAnalysis,让 JVM 自动优化栈上分配对象,减少堆压力。
  2. ​大对象直通老年代​​:

    • 调整 -XX:PretenureSizeThreshold(默认 0),避免小对象过早晋升。
  3. ​GC 日志分析​​:

    • 关注 Young GC 和 Full GC 频率、耗时及内存回收效率。

​六、调优总结​

​目标​​策略​
​降低延迟​选择 G1/ZGC,调整 -XX:MaxGCPauseMillis
​提高吞吐量​使用 Parallel GC,增大堆内存
​避免 OOM​监控内存使用,优化代码和配置

​关键原则​​:

  • 调优需结合应用场景(如高并发、大数据量)。
  • 避免过度调优,优先通过监控定位瓶颈。
  • 生产环境建议使用 G1/ZGC 等现代 GC 算法。

16.redis过期策略

. 定时删除(TTL-based Deletion)​

  • ​原理​​:
    在设置键的过期时间时,创建一个定时器(Timer),键到期时立即删除。
  • ​优点​​:
    ​内存精准释放​​,过期键立即清理,无内存浪费。
  • ​缺点​​:
    ​CPU 资源消耗高​​,大量键同时过期时,定时器触发可能引发性能抖动。
  • ​适用场景​​:
    对内存敏感但对延迟不敏感的场景(如缓存非关键数据)。

​2. 惰性删除(Lazy Deletion)​

  • ​原理​​:
    当客户端访问某个键时,Redis 检查其是否过期,若过期则删除。
  • ​优点​​:
    ​节省 CPU 资源​​,仅在访问时触发检查,避免无谓的删除操作。
  • ​缺点​​:
    ​内存泄漏风险​​,未访问的过期键长期占用内存。
  • ​适用场景​​:
    读多写少且过期键访问频率低的场景(如日志缓存)。

​3. 定期删除(Periodic Deletion)​

  • ​原理​​:
    周期性随机抽查一定数量的键,删除其中已过期的键。

  • ​优点​​:
    ​平衡 CPU 与内存​​,避免定时删除的 CPU 峰值和惰性删除的内存泄漏。

  • ​缺点​​:
    需合理配置抽查频率和数量,否则可能清理不彻底。

  • ​适用场景​​:
    通用场景,推荐生产环境默认选择。

Redis 的默认策略组合​

Redis 默认采用 ​​定期删除 + 惰性删除​​ 的组合策略:

  1. ​定期删除​​:

    • 默认每秒执行 10 次(由 hz 配置项控制)。
    • 每次随机检查 20 个键,删除其中过期的键。
    • 若过期键比例超过 25%,则继续扫描下一批。
  2. ​惰性删除​​:

    • 在 GETEXISTS 等命令执行时,检查键是否过期并删除。

内存淘汰策略(maxmemory-policy)​

当内存不足时,Redis 会根据策略主动删除键(与过期策略互补):

​策略​​描述​
volatile-lru从设置过期时间的键中,按 LRU 淘汰
allkeys-lru从所有键中按 LRU 淘汰
volatile-random从设置过期时间的键中随机淘汰
allkeys-random从所有键中随机淘汰
volatile-ttl从设置过期时间的键中,按剩余时间淘汰
noeviction不删除,返回错误(默认)

17.hashcode和equals

hashCode 是 Java 中 Object 类的一个方法,用于返回对象的哈希码。哈希码是一个整数值,它用于支持哈希表等数据结构的高效存储和检索操作。哈希码通常用于在集合(如 HashMapHashSet 等)中快速查找对象。

具体含义:

  • hashCode 方法为每个对象生成一个整数值(哈希码),这个值通常基于对象的内存地址或对象的内容计算出来。
  • 如果两个对象通过 equals() 方法被认为是相等的,它们的 hashCode() 也应该相等。
  • 然而,两个不同的对象的 hashCode() 不一定要不同,这意味着不同的对象可能会有相同的哈希码(这种情况叫做哈希冲突)。

为什么需要 hashCode

hashCode 的主要作用是在哈希表中快速定位对象。例如,在 HashMap 中,当我们使用一个键来查找一个值时,首先会计算键的 hashCode,然后将其映射到一个桶(bucket)中,最终进行比较以找到对应的值。这大大提高了查找效率。

hashCodeequals 是两个密切相关的方法,通常一起使用来决定对象的相等性

  • hashCode 用于返回对象的哈希值,主要用于哈希集合中查找和存储对象。
  • equals 用于判断两个对象的内容是否相同,通常用于对象比较。
  • 在自定义类中,如果重写了 equals,通常也需要重写 hashCode,以保证哈希集合的正确行为。

== 是 Java 中的比较操作符,用于比较两个变量或对象的内存地址数值equals() 是 Object 类中的方法,默认情况下也用于比较两个对象的引用地址,即判断两个对象是否为同一个对象。然而,许多类(如 StringInteger)都会重写 equals() 方法,使其能够比较对象的内容

  • 如果两个对象根据 equals 方法比较是相等的(即 obj1.equals(obj2) 返回 true),那么它们的 hashCode 方法必须返回相同的整数值。
  • 但是,如果两个对象的 hashCode 相同,并不一定意味着它们是相等的。这就需要进一步通过 equals 方法来验证。

为什么需要同时重写 hashCode 和 equals

  • 哈希表的高效性:哈希表(如 HashMapHashSet)是通过对象的 hashCode 来进行数据存储和查找的。如果你没有正确重写这两个方法,可能会导致哈希表在处理相同对象时出现错误,影响查找和删除操作的正确性。
  • 防止哈希冲突问题:通过自定义 hashCode 和 equals,可以避免哈希冲突的影响,确保哈希表操作的正确性。
// true
String a ="1";
String b ="1";
System.out.println(a==b);
System.out.println(a.equals(b));
System.out.println(a.hashCode()+"--"+b.hashCode());

// false
JSONObject a =new JSONObject();
JSONObject b =new JSONObject();
System.out.println(a==b);
System.out.println(a.equals(b));
System.out.println(a.hashCode()+"--"+b.hashCode());

哈希算法是将任意长度的输入数据映射为固定长度的输出值的函数,广泛用于计算机科学中的数据存储、数据校验、安全通信等多个领域。常见的哈希算法包括 MD5、SHA-1、SHA-256 等,其中 SHA-256 被认为在大多数应用中具有较好的安全性。

哈希冲突是不可避免的,因为哈希函数将无限大小的输入映射到有限的输出空间。对于关键应用,选择抗碰撞性强的哈希算法非常重要,如 SHA-256 或 SHA-3。同时,现代加密技术也可以通过“加盐”或“增强的加密协议”来降低哈希冲突对系统安全的威胁。

18.hashmap扩容机制

HashMap 在 Java 中是一个常用的键值对映射集合,它的扩容机制对于性能的优化非常重要。HashMap 的扩容是动态调整内部存储数组(即桶数组)的大小,以确保其在插入大量数据时依然能保持良好的查找性能。

1. HashMap 的内部结构

HashMap 使用一个 数组 和链表(或红黑树,在高负载时)来存储键值对数据。每个键值对通过哈希值被映射到数组的一个索引位置,如果不同的键有相同的哈希值,它们会被存储在同一个索引处,形成一个 链表(或红黑树)。当链表的长度超过一定阈值时,会将链表转化为红黑树来提高查找效率。

2. HashMap 扩容的触发条件

HashMap 默认的初始容量为 16,负载因子为 0.75。当插入的元素数量超过当前容量和负载因子的乘积时,HashMap 会触发扩容。扩容时,HashMap 会将容量翻倍,并重新计算所有键的哈希值并映射到新的数组中。具体的触发条件如下:

  • 负载因子:负载因子是一个衡量 HashMap 在扩容前填充的程度的参数,默认值为 0.75。它表示当 HashMap 中的元素数量达到 capacity * loadFactor 时,触发扩容。
  • 容量HashMap 的初始容量是 16,容量在扩容时会翻倍。即从 16 扩展到 32,再到 64,以此类推。

假设当前 HashMap 的容量为 n,负载因子为 0.75,当元素个数大于 n * 0.75 时,就会触发扩容。扩容后,新的容量为原来的两倍,并且元素会根据新的容量重新分配。

3. 扩容的具体流程

扩容是 HashMap 的一个相对昂贵的操作,因为它需要执行以下步骤:

  1. 分配新的数组:将原来数组的大小翻倍,创建一个新的数组,新的数组的大小是原数组的两倍。
  2. 重新哈希:将原来数组中的所有元素(键值对)重新计算哈希值,并根据新的数组容量,将元素重新分配到新的数组位置。由于数组容量的变化,哈希值映射的位置可能发生改变。
  3. 迁移元素:将所有元素从旧的数组迁移到新的数组中,这个过程需要遍历所有原数组中的元素,重新计算哈希并插入到新数组中。

4. 扩容对性能的影响

扩容通常是一个耗时的操作,因为它涉及到创建新数组并重新分配所有元素。扩容时,HashMap 的时间复杂度会变成 O(n) ,其中 n 是当前 HashMap 中元素的个数。因此,扩容操作会对性能产生一定影响,特别是在元素数量大时。

为了避免频繁的扩容,可以通过合理设置 HashMap 的初始容量和负载因子来减少扩容次数。例如,如果你已知 HashMap 将要存储的元素数量较大,可以通过 new HashMap(initialCapacity, loadFactor) 来初始化一个合适大小的 HashMap

5. 负载因子的作用

负载因子决定了 HashMap 何时进行扩容。较小的负载因子意味着 HashMap 会更早进行扩容,而较大的负载因子则会推迟扩容的发生。负载因子的默认值是 0.75,这通常在性能和内存消耗之间提供了一个平衡。

  • 较低的负载因子(例如 0.5)意味着 HashMap 会较早进行扩容,内存使用较为宽松,可能会提高查询性能。
  • 较高的负载因子(例如 0.9)会推迟扩容,减少内存消耗,但可能会导致查询性能下降,因为更多元素集中在同一个桶内,增加了碰撞的概率。

6. 扩容后的影响

扩容会导致 HashMap 的哈希冲突减少,因为桶的数量翻倍。扩容后,新的数组桶数量更大,哈希冲突的概率较小,从而可以提高查询和插入的效率。

7. 总结

  • HashMap 扩容的触发条件是元素数量达到当前容量与负载因子乘积的阈值。
  • 扩容时,容量会翻倍,并且所有元素会重新计算哈希值并重新分配到新的数组中。
  • 扩容会影响性能,尤其在大量数据插入时,可能会导致性能瓶颈。
  • 通过合理设置初始容量和负载因子,可以减少不必要的扩容操作,优化性能。