恒生电子 Java 一面 面经

1. Spring Boot自动配置原理是什么?和Spring有什么区别?

Spring Boot自动配置原理:

核心机制:

  • @SpringBootApplication注解包含三个关键注解
  • @EnableAutoConfiguration:启用自动配置
  • @ComponentScan:扫描组件
  • @SpringBootConfiguration:标识配置类

自动配置流程:

  1. Spring Boot启动时加载META-INF/spring.factories
  2. 读取所有EnableAutoConfiguration配置类
  3. 根据@Conditional条件判断是否生效
  4. 满足条件的配置类自动注入Bean

示例:

@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}

Spring vs Spring Boot:

配置方式

XML或Java配置

约定优于配置

依赖管理

手动管理版本

starter自动管理

内嵌服务器

需要外部容器

内嵌Tomcat/Jetty

启动方式

部署war包

jar包直接运行

开发效率

配置繁琐

开箱即用

核心优势:

  • 零配置快速开发
  • 统一的依赖管理
  • 生产级监控(Actuator)
  • 简化部署流程

2. MySQL索引的底层实现,B+树和B树的区别

MySQL索引结构:

InnoDB使用B+树的原因:

  • 所有数据存储在叶子节点
  • 叶子节点通过指针连接,支持范围查询
  • 非叶子节点只存索引,树高度低
  • 磁盘IO次数少

B树 vs B+树:

数据位置

所有节点都存数据

只有叶子节点存数据

叶子节点

不连接

通过指针连接

范围查询

需要中序遍历

叶子节点顺序扫描

查询稳定性

不稳定(可能在非叶子节点找到)

稳定(都要到叶子节点)

磁盘IO

相对多

相对少

B+树结构示例:

        [10, 20]
       /    |    \
    [5,8] [12,15] [25,30]
     /      |        \
  叶子节点 → 叶子节点 → 叶子节点
  (存储实际数据)

索引类型:

  • 主键索引(聚簇索引):叶子节点存完整数据行
  • 辅助索引(非聚簇索引):叶子节点存主键值
  • 联合索引:多个字段组合,遵循最左前缀原则

索引优化建议:

  • 选择区分度高的列建索引
  • 避免在索引列上使用函数
  • 使用覆盖索引减少回表
  • 合理使用联合索引

3. JVM内存模型,说说各个区域的作用

JVM内存结构:

1. 程序计数器(Program Counter)

  • 当前线程执行的字节码行号指示器
  • 线程私有
  • 唯一不会OOM的区域

2. 虚拟机栈(VM Stack)

  • 存储局部变量表、操作数栈、动态链接、方法出口
  • 线程私有
  • 每个方法执行创建一个栈帧
  • StackOverflowError:栈深度超限
  • OutOfMemoryError:栈扩展失败

3. 本地方法栈(Native Method Stack)

  • 为Native方法服务
  • 线程私有

4. 堆(Heap)

  • 存储对象实例和数组
  • 线程共享
  • GC主要区域
  • 分代:新生代(Eden + Survivor)、老年代

5. 方法区(Method Area)

  • 存储类信息、常量、静态变量、JIT编译代码
  • 线程共享
  • JDK8后改为元空间(Metaspace),使用本地内存

6. 运行时常量池

  • 方法区的一部分
  • 存储编译期生成的字面量和符号引用

内存分配示例:

public class Example {
    private static int staticVar = 1;  // 方法区
    
    public void method() {
        int localVar = 2;              // 虚拟机栈
        String str = new String("abc"); // str引用在栈,对象在堆
        final int constant = 3;         // 栈
    }
}

常见内存问题:

  • 堆溢出:对象过多,内存不足
  • 栈溢出:递归调用过深
  • 元空间溢出:加载类过多

4. 说说你对分布式事务的理解,有哪些解决方案?

分布式事务场景:

  • 跨数据库操作
  • 跨服务调用
  • 微服务架构下的数据一致性

解决方案:

1. 两阶段提交(2PC)

  • 准备阶段:协调者询问所有参与者是否可以提交
  • 提交阶段:所有参与者都同意则提交,否则回滚
  • 缺点:同步阻塞、单点故障、数据不一致风险

2. 三阶段提交(3PC)

  • CanCommit、PreCommit、DoCommit
  • 增加超时机制,减少阻塞
  • 仍然存在数据不一致问题

3. TCC(Try-Confirm-Cancel)

// Try阶段:预留资源
public void tryDeduct(String userId, BigDecimal amount) {
    // 冻结账户金额
    accountService.freeze(userId, amount);
}

// Confirm阶段:确认提交
public void confirmDeduct(String userId, BigDecimal amount) {
    // 扣减冻结金额
    accountService.deduct(userId, amount);
}

// Cancel阶段:回滚
public void cancelDeduct(String userId, BigDecimal amount) {
    // 解冻金额
    accountService.unfreeze(userId, amount);
}

4. 本地消息表

  • 业务操作和消息插入在同一事务
  • 定时任务扫描消息表发送消息
  • 消费方幂等处理

5. 消息队列(最终一致性)

  • 使用RocketMQ的事务消息
  • 半消息机制保证可靠投递
  • 适合对一致性要求不高的场景

6. Saga模式

  • 长事务拆分为多个本地事务
  • 每个本地事务有对应的补偿操作
  • 正向执行或反向补偿

实际选择:

  • 强一致性要求:2PC/3PC(性能差)
  • 最终一致性:消息队列、Saga(推荐)
  • 业务补偿:TCC(开发成本高)

5. HashMap的底层实现,为什么线程不安全?ConcurrentHashMap如何保证线程安全?

HashMap底层结构(JDK 1.8):

  • 数组 + 链表 + 红黑树
  • 初始容量16,负载因子0.75
  • 链表长度>8且数组长度≥64时转红黑树
  • 红黑树节点<6时退化为链表

put操作流程:

1. 计算key的hash值
2. 根据hash值定位数组下标:(n-1) & hash
3. 如果位置为空,直接插入
4. 如果位置有元素:
   - key相同则覆盖
   - 链表则遍历插入尾部
   - 红黑树则按树规则插入
5. 判断是否需要扩容(size > threshold)

HashMap线程不安全的原因:

1. 并发put导致数据丢失

// 两个线程同时put到同一位置
Thread1: 判断位置为空,准备插入A
Thread2: 判断位置为空,准备插入B
结果:只有一个元素被保存

2. 扩容时形成环形链表(JDK 1.7)

  • 多线程同时扩容
  • 链表rehash时形成环
  • get操作死循环,CPU 100%

3. size计数不准确

  • size++不是原子操作
  • 并发修改导致计数错误

Concurr

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Java面试圣经 文章被收录于专栏

Java面试圣经,带你练透java圣经

全部评论

相关推荐

评论
2
3
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务