面经(1)

1.hashmap的get与put流程

  1. put 流程

· 计算 hash(key)(高位异或低位,减少碰撞)

· 若数组为空则 resize() 初始化

· 索引定位:(n-1) & hash

· 若槽位为空则直接插入;否则检查首节点:

· key 相同则覆盖

· 不同则按链表或红黑树插入

· 链表长度 ≥8 且数组长度 ≥64 时转为红黑树

· 插入后 modCount++,检查 size > threshold 则扩容

  1. get 流程

· 计算 hash(key),定位槽位

· 若槽位非空则依次查找:先检查首节点(hash 和 key 匹配),否则按红黑树(getTreeNode)或链表(遍历)查找

· 找到返回 value,否则返回 null

  1. 扩容机制(JDK 1.8)

· 触发条件:

· 首次 put 初始化(默认容量 16)

· size > threshold(容量 × 负载因子 0.75)

· 链表长度 ≥8 但数组长度 <64 时优先扩容(避免树化)

· 过程:新容量 = 旧容量 × 2,新阈值也翻倍

· 迁移优化:

· 利用 (e.hash & oldCap) 判断新索引:0 则位置不变,否则 原索引 + oldCap

· 链表迁移保持原顺序(尾插法,避免死循环)

· 红黑树迁移后若某分支节点数 ≤6 则退化为链表

· 线程不安全:多线程下可能出现数据覆盖,建议用 ConcurrentHashMap

  1. 红黑树(解决哈希冲突严重时的性能退化)

· 树化条件:链表长度 ≥8 且 数组长度 ≥64;否则优先扩容

· 退化条件:红黑树节点数 ≤6 时转回链表

· 性质:自平衡二叉查找树,保证最坏情况 O(log n)

· 优点:插入、删除、查询均衡,比 AVL 树调整开销小,适合 HashMap 的频繁操作

· 节点结构:TreeNode 继承 LinkedHashMap.Entry,包含 parent、left、right、prev 和颜色标志

2.乐观锁以及aba问题,以及如何兜底

乐观锁通常用版本号或时间戳实现:更新数据时,校验当前版本号是否与读到的版本一致,一致则更新并将版本号+1,否则重试。

ABA问题是:T1将A改为B又改回A,T2在中间时段读取到A并通过CAS修改,可能错误地认为数据没变,但实际上已被改动过。这在金融、库存等敏感场景会引发严重问题。

兜底方案:

  1. 使用带版本号的原子类:如Java的AtomicStampedReference(同时维护引用和int戳记),或AtomicMarkableReference(维护引用和boolean标记),能区分A→B→A的变化。
  2. 全局递增版本号:每次更新不仅改数据,还把版本号严格递增(时间戳不够可靠,若系统时钟回拨仍可能ABA),版本号始终变大,回退版本号不可能。
  3. 数据库实现建议:表加version字段,更新时where version = oldVersion,且更新语句set version = version+1。由于数据库的隔离级别(如可重复读)和行锁机制,同一个事务内不会误判,ABA问题天然被版本号递增解决。
  4. 业务重试 + 异步补偿队列

同步重试策略

· 适用读多写少的乐观锁冲突场景(如库存扣减)。

· 建议指数退避:第1次等待10ms,第2次20ms,第3次40ms,最多3~5次。

· 前端配合:返回特定状态码,让用户手动刷新或自动重试。

异步补偿队列

当同步重试全部失败,或者业务不允许阻塞重试(如高并发秒杀),可以将请求写入消息队列(如RocketMQ、Kafka)。

· 消费者取到消息后,再次执行乐观锁更新,可重试多次。

· 最终仍失败则转入死信队列,人工介入或执行预先定义的补偿动作(比如记录失败库存流水,由定时任务对账修复)。

· 补偿动作要幂等:如扣减库存前检查"是否已处理过该订单"。

兜底效果

将瞬时冲突转化为最终一致,避免直接拒绝用户请求。


  1. 审计日志 + 数据修复机制

适用场景

金融、计费等绝对不能出错的数据。即使乐观锁重试全部失败,也不能丢失操作意图。

实现方式

· 单独建一张 change_log 表,记录每次数据变更前的旧值、新值、版本号、操作时间、操作人等。

· 更新数据时先写日志,后更新(可以在同一本地事务中,或基于binlog)。

· 若更新失败(包括乐观锁冲突),日志里有一条"尝试记录",标记为FAILED。

修复流程

  1. 定时任务扫描失败的变更记录,尝试根据当前版本号重新执行更新。
  2. 若仍失败且业务允许,则降级为"人工修复":展示新旧数据差异,提供界面让管理员确认后强制更新(可绕过乐观锁,但要二次审计)。
  3. 利用日志可回滚任意时刻状态,保证可追溯。

3.事件状态流转新增,删除,替换状态应该怎么做

方案一:外部化配置 + 状态机引擎

核心思想

将状态机的拓扑结构(状态、事件、转移规则)从代码中抽离,存储到外部配置中心(如 Nacos、Apollo)或关系数据库。应用启动时加载配置构建状态机实例,运行时监听配置变更,动态重建状态机并替换旧实例,从而实现状态的增、删、改而不重启服务。

技术选型建议

· Spring StateMachine:与 Spring 生态融合好,支持动态构建 StateMachine 对象。

· Squirrel StateMachine:轻量、无侵入,同样支持运行时定义。

· 自研简单引擎(若业务不复杂):基于 Map<State+Event, State> 存储规则,配合配置刷新。

详细实现步骤

  1. 定义配置数据模型

将状态、事件、转移规则以 JSON 或数据库表形式存储。

json 复制代码
// 示例:config.json
{
  "states": ["DRAFT", "SUBMITTED", "APPROVED", "REJECTED"],
  "events": ["SUBMIT", "APPROVE", "REJECT"],
  "transitions": [
    {"from": "DRAFT", "event": "SUBMIT", "to": "SUBMITTED"},
    {"from": "SUBMITTED", "event": "APPROVE", "to": "APPROVED"},
    {"from": "SUBMITTED", "event": "REJECT", "to": "REJECTED"}
  ]
}

数据库表设计(动态配置):

· state_config 表:id, config_version, states_json, events_json, transitions_json, enabled, create_time。

· state_change_log 表(审计):operator, action_type(ADD/DEL/REPLACE), old_config, new_config, create_time。

  1. 构建动态状态机工厂

使用 Spring StateMachine 的 StateMachineBuilder 动态构建。

java 复制代码
@Component
public class DynamicStateMachineFactory {
    private final Map<String, StateMachine<String, String>> machineCache = new ConcurrentHashMap<>();
    private volatile StateMachineConfig currentConfig;

    @PostConstruct
    public void init() {
        loadConfigFromNacos(); // 初次加载
        buildStateMachine();
    }

    // 监听配置变更(Nacos Listener)
    public void onConfigChange(String newJson) {
        StateMachineConfig newConfig = parseConfig(newJson);
        // 版本比对,若变化则重建
        if (!newConfig.equals(currentConfig)) {
            synchronized (this) {
                StateMachine<String, String> newMachine = buildMachine(newConfig);
                machineCache.put("default", newMachine); // 替换
                currentConfig = newConfig;
                // 可选:优雅关闭旧机器,释放资源
            }
            log.info("StateMachine rebuilt due to config change");
        }
    }

    private StateMachine<String, String> buildMachine(StateMachineConfig config) {
        StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();
        // 添加状态
        for (String state : config.getStates()) {
            builder.configureStates().withStates().initial(config.getInitialState())
                   .states(new HashSet<>(config.getStates()));
        }
        // 添加转移
        for (Transition t : config.getTransitions()) {
            builder.configureTransitions()
                   .withExternal()
                   .source(t.getFrom())
                   .target(t.getTo())
                   .event(t.getEvent());
        }
        return builder.build();
    }
}
  1. 动态新增状态(操作示例)

· 在配置中心(Nacos)修改 JSON,增加一个新状态 "CANCELLED",并增加从 DRAFT 到 CANCELLED 的 CANCEL 事件转移。

· 发布配置 → 应用监听器触发 onConfigChange → 重建状态机 → 新流转规则生效。

· 对于数据库存储,提供后台管理接口:POST /admin/state/add,插入新规则后调用 refresh() 重建。

  1. 删除状态与替换的注意事项

· 删除状态:必须先确保所有转移规则中不包含该状态作为 from 或 to,否则重建时会校验失败。建议在修改配置前做合法性检查:如要删除 APPROVED,先确认没有 from=APPROVED 的转移,若有则先删除或修改这些转移。

· 替换状态:例如将 APPROVED 重命名为 PASSED,本质是修改 states 列表中的名称,并将所有转移中的旧状态名替换为新名。需同步更新数据库中的业务数据(历史订单的状态字段值),属于数据迁移问题,一般通过脚本离线处理或提供灰度迁移工具。

  1. 运行时一致性保证

· 新老事务隔离:使用 ThreadLocal 或按业务主键(如订单号)绑定状态机版本号。简单做法:在动态重建时,旧的状态机实例不立刻销毁,而是通过引用计数等待正在使用它的请求完成(类似读写锁)。Spring StateMachine 本身默认是无状态的引擎(状态存储在上下文中),替换实例不影响已运行流程。

· 持久化状态:如果状态机状态需要持久化(如长流程),建议将状态机的当前状态和上下文都存到业务表。即使状态机定义变了,已有流程仍按旧规则完成,新的流程用新规则。配置变更时可以增加版本字段 rule_version,每个流程实例在创建时记录当时的规则版本。

  1. 审计与回滚

· 每次配置变更记录操作人、变更前后 JSON、时间,存到 state_change_log。

· 配置中心支持版本管理(如 Nacos 自带),可一键回滚到上一个版本,应用监听回滚后再次重建。

优缺点分析

优点 缺点

不重启即可调整流转逻辑,灵活度高 引入外部组件(配置中心/数据库),复杂度增加

适合多环境、多租户动态配置 需要处理并发构建和旧机器释放

可以复用成熟框架的功能(如监听器、动作、守卫) 状态定义与代码解耦后,部分编译期检查丢失


方案二:自研动态状态矩阵

核心思想

不使用外部状态机框架,而是将状态转移规则存储在内存中的一个 Map 结构里,并通过后台接口动态修改这个 Map。业务代码中直接调用状态转移服务,查询 Map 获取下一个状态,并执行校验与动作。

数据结构设计

java 复制代码
// 复合Key:当前状态 + 事件
public class TransitionKey {
    private String state;   // 当前状态
    private String event;   // 触发事件
    // equals & hashCode
}

// 转移规则值
public class TransitionValue {
    private String targetState;      // 目标状态
    private List<Action> beforeActions;  // 前置动作
    private List<Action> afterActions;   // 后置动作
    private Condition guardCondition;    // 守卫条件(可选)
}

核心存储:

java 复制代码
@Service
public class DynamicStateMatrix {
    // 主规则表:当前状态+事件 -> 下一状态及动作
    private volatile Map<TransitionKey, TransitionValue> matrix = new ConcurrentHashMap<>();
    // 辅助索引:记录每个状态作为源的所有规则,便于删除状态时快速清理
    private volatile Map<String, Set<TransitionKey>> sourceIndex = new ConcurrentHashMap<>();
    
    // 对外提供状态转移方法
    public StateContext transit(String currentState, String event, Map<String, Object> payload) {
        TransitionKey key = new TransitionKey(currentState, event);
        TransitionValue value = matrix.get(key);
        if (value == null) {
            throw new NoTransitionException();
        }
        // 执行守卫
        if (value.getGuardCondition() != null && !value.getGuardCondition().test(payload)) {
            throw new GuardFailedException();
        }
        // 执行前置动作
        for (Action action : value.getBeforeActions()) {
            action.execute(payload);
        }
        // 返回新状态
        String newState = value.getTargetState();
        // 执行后置动作
        for (Action action : value.getAfterActions()) {
            action.execute(payload);
        }
        return new StateContext(currentState, event, newState, payload);
    }
}

动态增删改接口实现

  1. 新增状态
java 复制代码
@PostMapping("/admin/transition/add")
public void addTransition(@RequestBody AddTransitionRequest req) {
    TransitionKey key = new TransitionKey(req.getFromState(), req.getEvent());
    TransitionValue value = new TransitionValue(req.getToState(), req.getActions());
    synchronized (this) {
        matrix.put(key, value);
        sourceIndex.computeIfAbsent(req.getFromState(), k -> ConcurrentHashMap.newKeySet()).add(key);
    }
    // 可选:持久化到数据库
    saveToDb(req);
}

· 注意:新增状态不需要提前声明状态列表,动态矩阵允许任意字符串作为状态名。

  1. 删除状态
java 复制代码
@PostMapping("/admin/state/delete")
public void deleteState(@RequestParam String state) {
    synchronized (this) {
        // 1. 查找所有以该状态作为源状态的规则
        Set<TransitionKey> fromRules = sourceIndex.getOrDefault(state, Collections.emptySet());
        if (!fromRules.isEmpty()) {
            throw new IllegalStateException("State " + state + " is source of transitions: " + fromRules);
        }
        // 2. 查找所有以该状态作为目标状态的规则(需要反向索引)
        Set<TransitionKey> toRules = targetIndex.getOrDefault(state, Collections.emptySet());
        if (!toRules.isEmpty()) {
            // 可以选择阻断删除,或者自动删除这些规则
            throw new IllegalStateException("State " + state + " is target of transitions: " + toRules);
        }
        // 3. 删除规则(实际上没有直接删除状态的概念,删除所有相关的规则即可)
        // 但通常还需标记该状态为不可用,并清理数据
    }
}

· 补充反向索引 Map<String, Set> targetIndex,在添加规则时同步维护。

  1. 替换状态(重命名)

· 例如将状态 "PENDING" 替换为 "WAITING"。

· 步骤:

  1. 扫描 matrix 中所有 TransitionKey 的 state 字段等于 "PENDING" 的,修改为 "WAITING"。
  2. 扫描所有 TransitionValue 的 targetState 等于 "PENDING" 的,修改为 "WAITING"。
  3. 重建索引。
  4. 注意:业务数据库中已存在的实体记录的 status 字段需要批量迁移,通常提供后台任务逐步更新。

运行时一致性及并发控制

· 使用 volatile 引用 + synchronized 进行写操作,确保修改规则时整个矩阵替换是原子操作。也可以采用 CopyOnWrite 模式:创建新 Map,填充新规则,最后一次性替换 matrix 引用。

· 对于正在进行的流转请求,由于引用替换是原子的,旧请求可能仍然使用旧矩阵(因为拿到的是替换前的引用),但这恰好保证了单次请求内的规则一致,不会出现半路规则变更导致状态错乱。若需要更严格的隔离,可以引入版本号,将版本存入事务上下文。

持久化与审计

· 每次通过接口修改规则时,同步写入数据库 transition_rule 表(带 version 或 is_active 标志),并插入审计日志。

· 应用重启后,从数据库加载最新规则到内存矩阵。

优缺点分析

优点 缺点

极轻量,无第三方依赖,实现简单透明 需要自己处理守卫条件、动作链、历史记录等功能

动态修改实时生效,性能极高(纯内存Map) 缺乏图形化监视和状态机可视化

完全可控,易于针对业务定制 对于复杂嵌套状态(子状态、并行状态)几乎无法支持


总结与选型建议

对比维度 方案一(外部化配置+引擎) 方案二(自研动态矩阵)

复杂度 中等(需要集成框架+配置中心) 低(只有集合操作)

功能丰富度 高(支持守卫、动作、子状态、历史状态等) 低(仅支持基础转移)

动态能力 优秀,配置变更触发重建 优秀,直接修改内存Map

适合场景 大型系统,状态>20,流转复杂,有专门运维团队 中小型系统,状态<15,流转简单,需快速迭代

对业务数据影响 需处理版本兼容性 同样需要处理状态字段迁移

最终建议:

· 若状态流转规则经常变化(每周超过2次)且团队规模较大 → 选方案一,并采用 Nacos + Spring StateMachine。

· 若只是偶尔调整(每月1次)且系统简单 → 选方案二,搭配简单后台管理界面即可。

· 无论哪种方案,都务必对删除状态做依赖检查,并建立配置变更的审计日志,否则运行时可能出现空转或数据残留。

4.bean希望在预生产有生产没有,有哪些方案

🎯 方案一:使用 @Profile 注解(最常用)

可以在配置类或 Bean 定义上使用 @Profile,并根据 Spring 激活的环境配置来决定是否加载。核心是让 Bean 仅在非生产环境激活。

示例代码:

java 复制代码
@Component
@Profile("!prod") // 当 'prod' 未激活时,此Bean才会被创建
public class YourPreProdBean {
    // 你的业务逻辑
}

如果你的预发布环境 Profile 名为 pre,也可以写成 @Profile("pre"),让 Bean 仅在 pre 激活时加载。

🔧 方案二:使用 @ConditionalOnProperty

除了 @Profile,@ConditionalOnProperty 也很常用。它能根据配置项的值,实现更精细的控制,适合将业务逻辑与通用的 Profile 名称解耦。

示例代码:

java 复制代码
@Component
@ConditionalOnProperty(
    name = "your.bean.enabled",  // 配置项名称
    havingValue = "pre",         // 期望值是 'pre' 时才加载
    matchIfMissing = false       // 配置项缺失时不加载
)
public class YourPreProdBean {
    // 你的业务逻辑
}

使用时,在预发布环境的配置文件中设置 your.bean.enabled=pre,生产环境不配置或设置其他值即可。

💡 方案三:其他备选方案

· 编程式的 @Conditional:当条件非常复杂时,可以自己实现 Condition 接口,灵活度最高。

· 构建时 Maven/Gradle Profile:通过 Maven 的 在打包阶段就排除掉特定环境的代码。虽然能保证生产包绝对干净,但环境隔离不够灵活。

· 配置文件占位符:这种方法主要用于切换依赖的 Bean,而非控制 Bean 本身是否加载。

5.有一个外部配置可能几个月才会变更一次,存他的时候用什么数据结构,以及读写的时候要注意什么,

针对"外部配置几个月才变更一次,如何存储及读写注意事项"的完整结论:

  1. 最佳数据结构

AtomicReference

· 用不可变对象(如 record、final 字段类)承载所有配置字段

· 用 AtomicReference 持有该对象的唯一实例

  1. 读写操作要点

· 读:直接 get() 获得当前快照,无锁、极快、线程安全

· 写:从远程拉取新配置 → 验证 → 构建完整新对象 → 原子替换(set 或 compareAndSet)

· 绝对禁止逐个字段修改,避免读线程看到部分更新

· 可用轻量锁防止并发写(低频操作,无影响)

  1. 其他注意事项

· 可见性:AtomicReference 已保证,无需额外 volatile

· 初始兜底:应用启动时必须内建默认配置或本地缓存文件,防止远端故障

· 变更通知:替换配置后,遍历回调列表通知业务模块(如有需要)

· 定时拉取:建议每小时或每天异步检查一次,而非实时监听

  1. 为什么不推荐其他结构

方案 问题

synchronized + 普通变量 每次读都要加锁,性能差

ReadWriteLock 仍有锁开销,不如原子引用轻量

ConcurrentHashMap 适合键值集合,难以保证整体配置的原子替换(可能读到部分字段更新)

CopyOnWriteArrayList 语义错误(列表装单个对象)、内存/性能冗余(每次写复制数组,读需get(0))、完全不是为此场景设计

  1. 一句话总结

用 AtomicReference 持有不可变配置对象,读无锁,写时整体原子替换;CopyOnWriteArrayList 是列表专用工具,不适用单个配置的存储。

相关推荐
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第77题】【Mysql篇】第7题:回表查询与全表扫描的区别?
java·开发语言·数据库·mysql·面试
张元清2 小时前
在 React 里写动画又不跟渲染周期较劲:useRafFn、useRafState、useFps、useDevicePixelRatio、useUpdate
前端·javascript·面试
代码帮2 小时前
面试题 - GIL全局解释器锁 :为什么Python多线程不能利用多核?GIL对I/O密集和CPU密集任务的影响?如何绕过GIL(多进程、C扩展)
python·面试
Raink老师3 小时前
【AI面试临阵磨枪-65】设计一个支持 10w 并发的 AI 聊天服务(流式、高可用、成本优化)
人工智能·面试·职场和发展
HelloRainy5 小时前
用 C++ 实现 shared_ptr 与 weak_ptr,线程安全是怎么解决的
面试
Java编程爱好者5 小时前
Kubernetes Pod 故障排查指南:从状态识别到根因定位的完整实践
面试
雮尘6 小时前
100+ React 面试题 —— 来自前面试官的直接整理(2026)
前端·react.js·面试
Mahir087 小时前
Spring MVC 深度解密:从 DispatcherServlet 到请求处理全流程
java·后端·spring·面试·mvc
一叶遮惊鸿7 小时前
Go 服务 Graph 热更新实践:用 atomic.Value 替代 sync.Once
面试