Java实战面试题(二)
2026/6/4 15:27:28 网站建设 项目流程

目录

1. Java中的锁有哪些?

2. 怎么保证 MySQL 中的原子性?

3. 缓存数据的一致性

4. HashMap 的底层,扩容机制,线程安全吗?

5. Spring Boot 自动装配原理是什么?

6. 商品库存扣减怎么保证数据正确?

7. 线程池的线程工厂有了解吗?

8. Java中乐观锁和悲观锁的原理与区别


1. Java中的锁有哪些?

回答思路:从不同维度分类,展现系统性理解。

按实现方式分

  • JVM内置锁synchronized(可修饰方法、代码块,锁升级机制偏向锁→轻量级锁→重量级锁)。

  • JUC显式锁ReentrantLock(可重入,公平/非公平可选,支持条件变量 Condition)、ReentrantReadWriteLock(读共享,写独占)。

按特性分

  • 乐观锁:先操作,再检查冲突。如 CAS(AtomicInteger)、版本号机制(数据库 version 字段)。

  • 悲观锁:假定冲突,先加锁。如synchronizedReentrantLock、数据库SELECT ... FOR UPDATE

  • 可重入锁synchronizedReentrantLock,同一线程可多次获取。

  • 读写锁ReentrantReadWriteLock,适合读多写少。

  • 自旋锁:CAS 循环重试,避免线程阻塞切换开销(synchronized重量级锁前就会自旋)。

  • 公平/非公平锁ReentrantLock构造函数可指定。

  • 分布式锁:基于 Redis(Redisson)、Zookeeper、数据库实现。

💬 面试答法:"Java 中的锁可以从多个维度分类。从实现上分内置锁 synchronized 和 JUC 下的显式锁如 ReentrantLock;从思想上分乐观锁和悲观锁;从功能上还有读写锁、可重入锁等。实际项目中,本地锁如 ReentrantLock 适用于单服务,跨服务场景则用 Redisson 分布式锁加 Lua 脚本保证原子性。"


2. 怎么保证 MySQL 中的原子性?

核心机制undo log(回滚日志)+ 事务机制

  • 原子性定义:事务中的所有操作要么全部成功,要么全部失败回滚,中途出错不能留下部分结果。

  • 实现原理

    1. 事务开始后,每次对数据的修改,InnoDB 都会先将旧数据写入 undo log。

    2. 如果事务执行中发生错误,或用户主动ROLLBACK,MySQL 会通过 undo log 中记录的旧值,将数据逐条回滚到事务开始前的状态

    3. 如果COMMIT,undo log 会被标记为可清除。

  • 辅助机制:redo log 配合保证事务提交后数据的持久性(D),与原子性配合构成 AID。

💬 面试答法:"MySQL 的原子性主要依靠 undo log 实现。事务修改数据前先记录旧值到 undo log,如果事务回滚,就用 undo log 里的旧值恢复数据。redo log 更多是保证持久性,两者共同保证 ACID 中的 A 和 D。"


3. 缓存数据的一致性

见前文第 4 题详细解答。核心要点

  • 标准方案Cache Aside(旁路缓存)—— 先更新数据库,再删除缓存

  • 极端情况兜底延迟双删(写完 DB,删一次缓存,延时几百毫秒再删一次)。

  • 最终一致性方案Canal 监听 binlog,投递 MQ,异步更新缓存

  • 确保底线:缓存设置合理过期时间,作为最终兜底。


4. HashMap 的底层,扩容机制,线程安全吗?

底层结构

  • JDK 1.8 之前:数组 + 单向链表。

  • JDK 1.8 及以后:数组 + 链表 +红黑树。当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树,提升查询效率(O(n) → O(log n))。

扩容机制(resize())

  1. 触发条件:当元素个数超过容量 × 负载因子(默认0.75)时。

  2. 扩容动作:创建一个原数组长度 2 倍的新数组。

  3. 数据迁移:遍历原数组每个位置(桶),重新计算每个元素在新数组中的位置。

    • 普通节点:通过hash & oldCap判断,0 留原位,1 去原位置 + 原数组长度

    • 链表:拆分为高低位两条链,分别迁移。

    • 红黑树:拆分为高低位两棵树,若拆分后树节点数 ≤ 6,退化为链表。

  4. 非原子性风险:扩容过程未加锁,多线程并发可能导致数据丢失形成环形链表(CPU 100%)。

线程安全吗?

  • HashMap 是非线程安全的

  • 替代方案

    • ConcurrentHashMap:JDK 1.8 使用CAS + synchronized锁住桶的首节点,只锁冲突桶,并发度高。不锁整个表,不允许存 null。

    • Hashtable:全表锁,每次锁整个表,并发低,已过时。

    • Collections.synchronizedMap(new HashMap()):同步包装器,性能也低。

💬 面试答法:"HashMap 底层是数组加链表加红黑树,链表过长会树化。扩容会创建一个两倍大小新数组,然后通过高低位拆分迁移数据。它本身不是线程安全的,多线程扩容可能产生死链。并发场景必须用 ConcurrentHashMap,它对每个桶的头节点用 CAS 加 synchronized 实现分段锁。"


5. Spring Boot 自动装配原理是什么?

核心注解@SpringBootApplication(包含@EnableAutoConfiguration)。

工作原理

  1. 入口@EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class)导入自动配置选择器。

  2. 定位候选配置类AutoConfigurationImportSelector会使用SpringFactoriesLoader,从 classpath 下所有META-INF/spring.factories文件中,加载 key 为org.springframework.boot.autoconfigure.EnableAutoConfiguration的所有类名。

  3. 条件过滤:这些候选类(如DataSourceAutoConfiguration)上通常有@ConditionalOnClass(类路径存在指定类)、@ConditionalOnMissingBean(容器中无此 Bean)、@ConditionalOnProperty(配置文件有指定属性)等条件注解。Spring Boot 会逐类评估这些条件。

  4. 按需装配:满足条件的类会被装载,并创建其声明的 Bean 注入容器。不满足条件的类被忽略。

  5. 自定义覆盖:开发者自己创建的 Bean 会因@ConditionalOnMissingBean而优先于自动配置生效。

💬 面试答法:"Spring Boot 通过@EnableAutoConfiguration扫描spring.factories文件中列出的所有自动配置类,再根据@Conditional系列注解按需装配。比如你引入了 MySQL 驱动,它就会自动给你创建DataSourceBean,但如果你自己配置了,它就退让。"


6. 商品库存扣减怎么保证数据正确?

这是你售电/项目经验中的常见场景,回答时要突出数据库层面的强一致性。

方案一:数据库行锁(悲观锁)—— 简单可靠

SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- 加行锁
  • 在事务中先用FOR UPDATE锁住该行,再判断stock >= 扣减值,然后UPDATE stock = stock - num

  • 优点:强一致性,不会超卖,实现简单。

  • 缺点:高并发下大量请求串行等待,性能瓶颈。

  • 优化:可将扣减请求放入队列异步串行处理,前端提示“排队中”。

方案二:乐观锁(版本号/CAS)—— 适合并发不那么极端

UPDATE product SET stock = stock - #{num}, version = version + 1 WHERE id = #{id} AND stock >= #{num} AND version = #{oldVersion};
  • 判断受影响行数,0 表示冲突,重试或提示。

  • 缺点:并发极高时,大量重试可能压死数据库。

方案三:Redis 分布式锁/原子扣减

  • 使用 Redis 的DECRBY原子操作预扣,或 Redisson 加锁后扣减。但需处理缓存和 DB 的一致性问题。

💬 面试答法:"核心是防止超卖。单服务下推荐直接用数据库行锁SELECT FOR UPDATE,在事务中检查和扣减,强一致性最高。如果并发更高,可以引入 Redis 预减库存做挡板,但一定要通过异步落库和最终一致性方案保证库存正确。乐观锁重试成本太高,扣减场景不优先推荐。"


7. 线程池的线程工厂有了解吗?

定义ThreadFactory是一个接口,用来创建线程。Executors.defaultThreadFactory()创建的线程默认优先级为NORM_PRIORITY,非守护线程,且有统一命名(pool-x-thread-y)。

为什么需要自定义?

  1. 可识别的线程命名:设成order-pool-thread-1,方便排查堆栈信息、死锁和性能瓶颈。

  2. 设置守护线程:主线程结束时希望线程池也关闭,可设setDaemon(true)

  3. 设置异常处理器:通过setUncaughtExceptionHandler捕获线程内未处理异常,做日志记录。

示例

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat("order-handler-%d") // 命名格式 .setDaemon(false) // 非守护 .build();

Guava 的ThreadFactoryBuilder是其常用实现,简洁可靠。

💬 面试答法:"自定义线程工厂主要为了给线程起有业务含义的名字,并可以统一设置异常处理器。推荐用 Guava 的ThreadFactoryBuilder,代码简洁。实际开发中如果不用自定义名,排查线上问题看到一堆pool-1-thread-1很难定位。"


8. Java中乐观锁和悲观锁的原理与区别

维度悲观锁乐观锁
思想认为并发操作大概率冲突,每次操作都加锁认为冲突是小概率,不加锁,更新时检查数据是否被改
实现synchronized,ReentrantLock, 数据库行锁SELECT ... FOR UPDATECAS(Compare And Swap)、数据库版本号(version字段)
适用场景写多读少,竞争激烈,锁持有时间短读多写少,冲突较少
优点强一致性,不会出现更新丢失,实现简单无锁操作,不会死锁,并发读性能高
缺点锁竞争导致线程阻塞、上下文切换开销大,易死锁冲突严重时循环重试耗 CPU;CAS 有 ABA 问题;只能保证一个共享变量的原子操作
经典案例商品库存扣减(防超卖)用户积分更新、文档协同编辑

💬 面试答法:"悲观锁假定冲突必然发生,所以先拿锁再操作,比如数据库行锁扣库存;乐观锁假定冲突少,更新时通过版本号或 CAS 检查是否被改过,改了就不覆盖。实际应用看场景:下单扣库存用悲观锁,用户个人资料更新用乐观锁。"

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询