Java:==equals()hashCode()(HashMap)

我的理解偏差

  1. == 只理解成“比较引用地址”

    • 这只适用于引用类型。
    • 对基本数据类型,== 比较的是值。
    • 对引用类型,== 判断两个引用是否指向同一个对象。
  2. equals() 说成“判断是不是同一个对象”

    • 更准确的说法是:equals() 用于判断两个对象是否“逻辑相等”。
    • 两个不同的对象,也完全可以因为业务字段相同而 equals() == true
  3. hashCode() 理解成可以直接定位到某个对象

    • hashCode() 主要用于散列查找,帮助 HashMap 快速缩小查找范围、定位桶。
    • 不同对象可能拥有相同的 hashCode,因此 hashCode 不能唯一确定一个对象。
    • HashMap 找到桶后,还需要通过 equals() 进一步确认 key。
  4. “重写 equals 必须重写 hashCode”的关键原因没有说完整

    • Java 约定:如果 a.equals(b) == true,则必须保证 a.hashCode() == b.hashCode()
    • 在 HashMap 中,如果两个逻辑相等的 key 因为 hashCode 不同被定位到不同的桶,那么查找时连 equals() 都可能没有机会执行,最终导致本应相等的 key 查找失败。

核心记忆

hashCode() 负责“找桶”,equals() 负责“认人”。

如果 equals() 相等但 hashCode() 不同,两者可能被放进不同的桶,甚至没有机会进行 equals() 比较。

Java:ArrayList 扩容机制与 ArrayList / LinkedList 适用场景

我的理解偏差

  1. 把 ArrayList 的“首次扩容为 10”说成初始化时就有 10 个空间

    • 使用默认构造器 new ArrayList<>() 时,底层一开始通常是空数组。
    • 第一次真正添加元素时,才会把容量扩到默认容量 10。
  2. 把 ArrayList 扩容理解成“只有装满后才固定扩 1.5 倍”

    • 常见情况下,新容量为:oldCapacity + (oldCapacity >> 1),也就是约 1.5 倍。
    • 但本质上是“当前容量不足以容纳新元素时”触发扩容;如果一次性需要的容量比 1.5 倍还大,会直接扩到能够满足需求的容量。
    • 扩容后会创建新数组,并把旧数组元素复制过去。
  3. 把 LinkedList 简单理解成“经常增删就应该用 LinkedList”

    • LinkedList 在“已经定位到节点”的前提下,插入和删除本身可以做到 O(1)。
    • 但如果要先按下标找到目标位置,查找过程是 O(n),因此“查找 + 增删”整体仍可能是 O(n)。
    • ArrayList 在尾部追加元素通常是均摊 O(1),并不是所有新增操作都很慢。
  4. 没有强调 ArrayList 最大的优势是随机访问和缓存友好性

    • ArrayList 底层是连续数组,按下标访问是 O(1),CPU 缓存局部性也更好。
    • LinkedList 按下标访问需要从头或尾逐个节点寻找,随机访问是 O(n),而且每个节点还要额外保存前后指针。

核心记忆

ArrayList:查得快、尾部追加也快;中间插删需要搬元素。

LinkedList:节点插删本身快,但“先找到这个节点”往往不快。

不要死记“增删多用 LinkedList”;很多实际场景中,即使存在增删,ArrayList 仍然可能更合适。

Java / MySQL:乐观锁与悲观锁

我的理解偏差

  1. SELECT ... FOR UPDATE 理解成“锁住该行后,其他事务什么都不能做”

    • SELECT ... FOR UPDATE 会对目标记录加悲观锁,阻止其他事务进行冲突性的加锁或修改操作。
    • 但普通 SELECT 在 InnoDB 的 MVCC 机制下,很多情况下仍然可以读取旧版本数据,并不一定会被阻塞。
  2. 把 version 乐观锁理解成“发现版本不一致后,数据库会不断重试直到成功”

    • version 字段的核心作用是“检测并发冲突”,而不是自动重试。
    • 更新时通常使用:WHERE version = oldVersion,并同时执行 version = version + 1
    • 如果受影响行数为 0,说明数据已经被其他事务修改;是否重新查询并重试,由业务代码决定。
  3. 把 CAS 描述成“CPU 支持,所以不会出岔子”

    • 更准确地说,CAS 的“比较并交换”本身是原子操作,通常由 CPU 原子指令保证。
    • CAS 并不代表所有并发问题都会自动消失,例如还存在 ABA、自旋开销等问题。

核心记忆

悲观锁:先锁,再操作。

乐观锁:先操作,提交修改时再检查数据有没有被别人改过。

Java:synchronized 体现悲观锁思想;AtomicInteger 的 CAS 体现乐观并发思想。

MySQL:SELECT ... FOR UPDATE 是典型悲观锁;version 字段是常见乐观锁实现。

乐观锁 ≠ 自动重试。version 负责发现冲突,是否重试由程序决定。

Redis:分布式锁的加锁与解锁

我的理解偏差

  1. 把分布式锁写成 SETNX 后再单独设置 TTL

    • SETNXEXPIRE 如果分成两条命令,中间一旦程序宕机,可能出现“锁已经加上,但永不过期”的死锁。
    • 应使用一条原子命令完成“加锁 + 设置过期时间”,例如:SET lock:key <uniqueToken> NX PX 30000
  2. 把获取锁失败后的等待理解成 CAS 自旋

    • Redis 分布式锁的竞争并不是 Java CAS。
    • 获取失败后可以直接返回失败,也可以采用“重试 + sleep/退避/随机抖动”等方式再次尝试;不应无间隔地疯狂轮询 Redis。
  3. 只强调“TTL 不能短于业务执行时间”,但没有考虑业务超时和锁续期问题

    • TTL 的主要作用是防止持锁客户端异常退出后形成永久死锁。
    • 如果业务执行时间超过 TTL,锁会提前失效,其他客户端可能拿到同一把锁,导致两个客户端同时进入临界区。
    • 对执行时间不可预测的任务,需要设置合理 TTL,并考虑自动续期(watchdog)等机制。
  4. 认为 finally 里直接 DEL key 就足够安全

    • finally 解锁只能保证“尽量执行释放”,但不能保证“释放的是自己的锁”。
    • 每个持锁者都应把一个唯一 token(如 UUID)写入 value;解锁时只有 value 仍然等于自己的 token 才能删除。
    • “比较 token + 删除 key”必须是原子操作,通常使用 Lua 脚本完成;不能先 GETDEL,否则两条命令之间可能发生竞态。
  5. 没有考虑 Redis 主从切换带来的锁安全问题

    • 在主从架构中,如果主节点刚写入锁但还未来得及复制就宕机,故障转移后新主节点可能没有这把锁,其他客户端就可能再次获得锁。
    • 因此对强一致性要求很高的场景,需要评估 Redis 分布式锁在故障切换下的语义,或使用更适合强一致协调的方案。

核心记忆

加锁:SET lock:key <唯一token> NX PX <TTL>加锁和设置过期时间必须原子完成

解锁:只能删自己的锁;用唯一 token 标识所有者,并通过 Lua 原子执行“比较 token + 删除”。

TTL 防止死锁,但 TTL 太短会导致“业务还没做完,锁先没了”;执行时间不可预测时要考虑续期。

获取失败不是 CAS;重试时应避免无间隔自旋打爆 Redis。

Spring:Bean 生命周期主要阶段

我的理解偏差

  1. BeanDefinition 写成 BeanDefination,并把它理解成“根据 BeanDefinition 初始化 Bean”

    • 更准确地说,Spring 先根据 BeanDefinition 创建 Bean 实例,也就是“实例化”;此时对象本身刚被创建出来,依赖还没有全部注入。
  2. PostProcessBean 当成生命周期组件名称

    • 正确名称是 BeanPostProcessor
    • 初始化前调用 postProcessBeforeInitialization(),初始化后调用 postProcessAfterInitialization()
  3. 只写了 @init,没有区分真正的初始化回调

    • 常见初始化回调包括 @PostConstructInitializingBean.afterPropertiesSet() 和自定义 init-method
    • 它们发生在 BeanPostProcessor 的“初始化前”与“初始化后”之间。
  4. 把“BeanPostProcessor 后初始化”直接等同于“完成 AOP 代理”

    • AOP 代理通常确实是在某些 BeanPostProcessorpostProcessAfterInitialization() 阶段创建的。
    • 但不是所有 Bean 都一定会被 AOP 代理,因此不能把生命周期第 6 步写成“必然完成 AOP 代理”。
  5. 把销毁阶段简单写成 @destroy

    • 常见销毁回调包括 @PreDestroyDisposableBean.destroy() 和自定义 destroy-method
    • 一般是在 Spring 容器关闭时触发;prototype Bean 的完整销毁生命周期通常不由 Spring 容器负责。

核心记忆

Spring Bean 生命周期主线:实例化 → 属性赋值/依赖注入 → Aware 回调 → BeanPostProcessor 初始化前 → 初始化回调 → BeanPostProcessor 初始化后 → Bean 可用 → 容器关闭时执行销毁回调。

BeanPostProcessor前处理 → 初始化 → 后处理

AOP 代理通常在 postProcessAfterInitialization() 阶段产生,但不是每个 Bean 都一定产生代理

Spring Bean 生命周期:销毁注解

我的理解偏差

  • 误以为 Spring Bean 生命周期中存在 @Destroy 注解。

正确理解

  • Spring / Jakarta 常用的销毁回调注解是 @PreDestroy,不是 @Destroy
  • Spring Boot 3 / Spring Framework 6 使用 jakarta.annotation.PreDestroy
  • Bean 的常见销毁方式包括:
    1. @PreDestroy
    2. DisposableBean.destroy()
    3. 自定义 destroy-method

核心记忆

初始化常见 @PostConstruct,销毁常见 @PreDestroy。 没有标准的 @Destroy

Spring Bean 生命周期:复答补充

本次仍需补充的点

  1. 初始化方法列举不完整

    • 除了 @PostConstruct 和自定义 init-method,还常见 InitializingBean.afterPropertiesSet()
  2. 第 6 步不能直接写成“执行 AOP 增强”

    • 更准确地说:执行 BeanPostProcessor#postProcessAfterInitialization();如果某个 Bean 需要 AOP 增强,相关自动代理创建器可能在这一阶段返回代理对象。
    • AOP 代理不是每个 Bean 都必经的生命周期步骤。

核心记忆

初始化阶段常见顺序:@PostConstructInitializingBean.afterPropertiesSet() → 自定义 init-method

postProcessAfterInitialization() 是扩展点;AOP 只是这个扩展点的一种典型用途。