思格新能源后端开发面试:JUC 与 MySQL/Redis 八股
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- JUC 并发编程中你常使用的工具类有哪些?
- 线程池的一些核心配置和参数有哪些?
- 进程、线程、协程的区别是什么?
- 介绍一下 MySQL 的索引和索引类型。
- 介绍一下 Redis 锁的实现。
- Bean 的生命周期,以及 IOC 和 AOP。
- 类加载机制了解吗?
- 实习项目的一些指标(提问)。
- MQ 想实现顺序性消费,怎么实现?
《参考解析》
JUC 常用工具类怎么答:不要报菜名,按用途分组并各配一句场景——并发容器(ConcurrentHashMap 做本地缓存与计数、CopyOnWriteArrayList 适合读多写极少的监听器列表、BlockingQueue 做生产者消费者解耦);同步工具(CountDownLatch 等一批任务跑完、CyclicBarrier 可复用分阶段、Semaphore 控并发度做限流);锁(ReentrantLock 可中断/可超时/公平锁、ReadWriteLock 读多写少、StampedLock 乐观读);原子类与 LongAdder(高并发计数比 AtomicLong 的 CAS 自旋省得多);CompletableFuture(并发编排与超时兜底);ThreadPoolExecutor 本身。每类能讲出”为什么选它而不是另一个”就够好了。
线程池核心参数与调优:七个参数是 corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。关键在任务提交顺序:核心线程未满就建线程 → 队列未满就入队 → 队列满了才把线程扩到 max → 再满就触发拒绝策略。这里最容易被追问的是队列选择:用了无界的 LinkedBlockingQueue,maximumPoolSize 永远用不上,任务会一直堆积直到 OOM。拒绝策略四种(AbortPolicy 抛异常、CallerRunsPolicy 让提交线程自己跑形成背压、DiscardPolicy/DiscardOldestPolicy 丢弃),生产上一般自定义:记录日志 + 落库重试。核心线程数经验值:CPU 密集型取核数 + 1,IO 密集型取核数 ×(1 + 等待时间/计算时间),但必须用压测校准,并说明线程池隔离(不同业务用不同池,别互相拖死)。
进程、线程、协程:进程是资源分配单位,有独立地址空间;线程是 CPU 调度单位,同一进程内共享内存,Linux 里线程就是用 clone 实现的轻量级进程,切换要进内核、开销在微秒级;协程是用户态调度,切换不陷内核、开销在纳秒级,代价是一个协程阻塞会堵住承载它的线程,所以要配合非阻塞 IO。可以补一句 Java 21 的虚拟线程(Thread.ofVirtual)就是 JVM 层面的协程,适合高并发 IO 密集场景,CPU 密集场景没有收益。
MySQL 索引与索引类型:InnoDB 用 B+ 树,原因是矮胖(三到四层能放两千万行,磁盘 IO 少)、叶子节点成链支持范围扫描、非叶节点只存键不存数据。要能区分聚簇索引(主键即数据,二级索引叶子存主键值,所以要回表)与二级索引;索引类型有主键、唯一、普通、联合、前缀、全文;联合索引遵守最左前缀,能被覆盖索引避免回表,还有索引下推(Using index condition)。索引失效的典型:对列做函数或隐式类型转换、以 % 开头的模糊匹配、or 连接非索引列、不满足最左前缀。工程上要会看 EXPLAIN 的 type/key/rows/Extra(Using filesort、Using temporary 是优化信号)。
Redis 锁怎么实现:基础版是 SET key uniqueValue NX PX 30000——NX 保证互斥、PX 防死锁、唯一 value 保证只能由持有者释放;释放必须用 Lua 原子地”比较 value 再删”,否则可能删掉别人的锁。业务没执行完锁过期的问题,靠看门狗续期(Redisson 默认每 10 秒续到 30 秒)或把过期时间设得足够长并做幂等。可重入可以用 Hash 存(key → 线程标识 → 重入次数)。要主动说清局限:单点 Redis 挂掉或主从切换时锁可能丢,Redlock 的争议在于依赖各节点时钟;更稳的做法是业务侧用 fencing token(带上单调递增的版本号,让被保护的资源自己拒绝过期持有者)或干脆用数据库唯一约束/状态机兜底。
Bean 生命周期与 IOC/AOP:生命周期主干是:实例化(构造)→ 属性填充(依赖注入)→ Aware 回调(BeanNameAware/BeanFactoryAware/ApplicationContextAware)→ BeanPostProcessor.postProcessBeforeInitialization → 初始化(@PostConstruct → InitializingBean.afterPropertiesSet → init-method)→ BeanPostProcessor.postProcessAfterInitialization(AOP 代理就是在这里生成的)→ 使用 → 销毁(@PreDestroy/DisposableBean)。IOC 是容器接管对象的创建与依赖装配,好处是解耦与可替换;AOP 是基于代理(JDK 动态代理需接口、CGLIB 走子类)织入横切逻辑,典型场景是事务、日志、权限、缓存。被追问时能说清”同类内部方法自调用会导致事务/缓存注解失效”这类实际坑,加分很多。类加载机制:加载 → 验证 → 准备(静态变量给零值)→ 解析 → 初始化(执行 <clinit>),双亲委派保证核心类不被篡改,打破双亲委派的场景是 SPI(Thread.currentThread().getContextClassLoader())、Tomcat 的 Web 应用隔离和热部署。
MQ 顺序消费怎么做:核心是两步——同一业务 key 必须落到同一队列/分区,消费端对该队列单线程串行消费。生产端:Kafka 用 partition key(订单号)保证同分区有序,RocketMQ 用 MessageQueueSelector 按订单号选队列;消费端:Kafka 一个分区只能被一个消费者线程消费,RocketMQ 的顺序消费(MessageListenerOrderly)会对队列加锁并把消息投给同一个线程。注意顺序被破坏的常见来源:重试——失败消息进重试队列后重新投递时可能插到后面(要么阻塞当前队列重试,要么把失败消息落库后由业务补偿);扩容与分区数变更会打乱既有 key 的分配;异步发送 + 多线程并发发送同一个 key 也可能乱序。最后一定要提幂等:顺序 + 至少一次投递,去重靠业务唯一键或状态机。