Shopee Java 一面问题的简要解答思路,可作为面试备考参考:

1. java 中的锁机制,什么是可重入锁

  • Java 锁机制:包含 synchronized 关键字(内置锁,基于对象监视器)、ReentrantLock 等显式锁(基于 AQS 实现),还有读写锁(ReentrantReadWriteLock )、自旋锁、偏向锁、轻量级锁、重量级锁等优化策略,用于解决多线程并发访问共享资源的同步问题 。
  • 可重入锁:也叫递归锁,指同一线程外层函数获取锁后,内层递归调用或同一类其他同步方法调用时,能直接再次获取该锁,不会因已持有锁而阻塞。synchronized 和 ReentrantLock 都是可重入锁,实现原理是锁内部记录持有锁的线程及重入次数,释放时次数递减,减到 0 才真正释放锁 。

2. AQS

AQS(AbstractQueuedSynchronizer )是 Java 并发包(java.util.concurrent.locks )中很多同步组件的基础框架,像 ReentrantLock、Semaphore、CountDownLatch 等都基于它实现。

  • 核心思想:通过一个 int 类型的状态变量(state )表示同步状态,利用 FIFO 队列(CLH 变体)管理等待获取同步状态的线程。
  • 主要方法:tryAcquire(尝试获取锁,自定义同步逻辑)、tryRelease(尝试释放锁)、acquire(获取锁,失败则入队等待)、release(释放锁,唤醒等待线程 )等,子类通过重写 tryAcquiretryRelease 等方法,实现独占锁、共享锁等不同同步语义 。

3. redis 相关数据结构,为什么每种数据类型一般都有两种数据结构?

  • Redis 基本数据结构:有字符串(string )、列表(list )、哈希(hash )、集合(set )、有序集合(sorted set ),每种底层由不同编码的内部数据结构实现,比如:
    • string 可由简单动态字符串(SDS )或长整型等编码;
    • list 用快速链表(quicklist ,是双向链表和压缩列表的组合 )或压缩列表(ziplist ,节省内存,数据量小时用 );
    • hash 用压缩列表或哈希表(dict );
    • set 用整数集合(intset )或哈希表;
    • sorted set 用压缩列表或跳表(skiplist )+ 哈希表 。
  • 存在两种数据结构原因:Redis 为了在不同场景下平衡性能和内存占用。数据量小、访问模式简单时,用压缩列表等紧凑结构节省内存;数据量大或有复杂操作(如快速查找、遍历 )时,切换到哈希表、跳表等更高效支持操作的结构,提升读写性能,实现“高效且省内存”的设计目标 。

4. jvm 相关 内存结构,gc

  • JVM 内存结构
    • 程序计数器:线程私有,记录当前线程执行字节码的行号,是唯一不会 OOM 的区域 。
    • 虚拟机栈:线程私有,存储局部变量、操作数栈、方法出口等,栈深度过大会抛 StackOverflowError ,线程请求栈深度超过虚拟机允许深度;或动态扩展栈时无法申请足够内存,会抛 OutOfMemoryError
    • 本地方法栈:类似虚拟机栈,为 Native 方法服务,也会抛栈溢出或 OOM 。
    • :线程共享,存储对象实例和数组,是 GC 主要区域,分新生代(Eden 、Survivor 区,From 和 To )、老年代,可通过 -Xms(初始堆大小 )、-Xmx(最大堆大小 )等参数调整,堆内存不足会抛 OOM 。
    • 方法区(元空间在 HotSpot 中替代永久代 ):线程共享,存储类信息、常量、静态变量、即时编译器编译后的代码等,元空间默认使用本地内存,也可能出现 OOM 。
  • GC(垃圾回收 )
    • 判断对象存活:常用引用计数法(存在循环引用无法回收问题,Java 未采用 )和可达性分析(以 GC Roots 为起点,遍历对象引用链,无引用链相连的对象可回收 )。
    • 垃圾回收算法
      • 标记 - 清除:标记可回收对象,然后清除,会产生内存碎片 。
      • 标记 - 整理:标记后,让存活对象向一端移动,消除碎片,用于老年代 。
      • 复制算法:将内存分成两块,只用一块,存活对象复制到另一块,清空原块,新生代(Eden 到 Survivor )常用(From 、To 区复制 )。
      • 分代收集:基于对象“朝生夕灭”特点,新生代用复制算法,老年代用标记 - 整理或标记 - 清除算法 。
    • 垃圾收集器:Serial(单线程 ,新生代 )、ParNew(Serial 多线程版 ,新生代 )、Parallel Scavenge(关注吞吐量,新生代 )、Serial Old(Serial 老年代版 )、Parallel Old(Parallel Scavenge 老年代版 )、CMS(并发标记清除,老年代,低停顿 ,但有碎片、浮动垃圾问题 )、G1(区域化分代,兼顾吞吐量和停顿,可预测停顿时间 )、ZGC(低延迟,大内存场景 )等,不同收集器适用不同场景 。

5. hashmap 底层原理

  • JDK7 及之前:数组 + 链表。初始化时创建数组(默认大小 16 ,负载因子 0.75 ),元素通过 hash(key.hashCode()) 计算哈希值,确定在数组的索引位置,哈希冲突时,元素以链表形式挂在数组对应位置 。
  • JDK8 及之后:数组 + 链表 + 红黑树。当链表长度(元素个数 )大于等于 8 且数组长度大于等于 64 时,链表转为红黑树;当红黑树节点数小于 6 时,转回链表。这样在哈希冲突严重时,提升查询效率(链表查询是 O(n) ,红黑树是 O(logn) )。put 时先计算哈希,找桶位置,判断是否是头节点,是则替换,否则遍历链表或树,看是否 key 已存在,存在则替换值,不存在则新增,新增后检查是否需扩容或树化;get 时同样先找桶,再遍历链表或树匹配 key 获取值 。
  • 扩容:当元素数量达到 容量 * 负载因子 时,进行 2 倍扩容(JDK8 是扩容为原数组长度的 2 倍 ),扩容时需重新计算元素在新数组的位置,JDK8 利用高低位哈希,减少元素移动次数 。

6. mysql 索引类型,分别解释,索引失效,什么是覆盖索引,什么是 hash 索引,和 b + 树有什么区别?

  • 索引类型
    • 主键索引:主键字段上创建,唯一且非空,一个表只能有一个主键索引,加速主键查询 。
    • 唯一索引:索引列值必须唯一(允许空值,主键索引不允许 ),可用于身份证号等字段,避免重复 。
    • 普通索引:最基本索引,无唯一性约束,用于加速查询 。
    • 组合索引:多个字段组合创建索引,遵循“最左匹配原则”,查询时按索引字段顺序使用才能有效利用 。
    • 全文索引:用于文本内容的模糊查询(如文章正文 ),需特定存储引擎(如 MyISAM 、InnoDB 5.6+ 支持 ),且要先建立全文索引,再用 MATCH...AGAINST 语法查询 。
  • 索引失效场景
    • 查询条件中对索引列进行运算(如 where id + 1 = 5 )、函数操作(如 where substring(name,1,1) = 'a' )。
    • 索引列使用 !=<> 等不等于操作(某些情况可能走索引,视数据分布 ),或 is nullis not null (数据分布影响 )。
    • 模糊查询以 % 开头(如 like '%xxx' ),无法利用索引,而 like 'xxx%' 可走索引 。
    • 违反最左匹配原则(组合索引,查询未按索引字段顺序,且中间跳过 )。
    • 数据类型隐式转换(如索引列是字符串,查询用数字,where name = 123 ,实际是 where name = '123' ,可能导致索引失效 )。
  • 覆盖索引:查询的列正好是索引包含的列,无需回表查询(即通过索引就能获取到查询所需全部数据 )。比如表有 id(主键 )、nameage 字段,创建 (name, age) 索引,查询 select name, age from table where name = 'xxx' ,就可利用覆盖索引,提升查询效率 。
  • hash 索引:基于哈希表实现,对索引键计算哈希值,映射到哈希表位置存储,查询时快速定位。InnoDB 中自适应哈希索引是一种优化,自动为热点页建立哈希索引。
  • hash 索引与 B + 树索引区别
    对比项 Hash 索引 B + 树索引
    结构原理 哈希表结构,键 -> 哈希值 -> 存储位置 平衡树结构,叶子节点有序且相连,存数据或主键
    查询方式 等值查询快(直接找哈希值对应位置 ),范围查询无法支持 等值、范围查询都能高效支持(遍历叶子节点链表 )
    排序支持 不支持索引键排序,查询结果需额外排序 叶子节点有序,查询结果可直接按索引顺序返回
    重复键处理 哈希冲突时,链表或其他方式解决 重复键可存多个,不影响结构
    适用场景 等值查询频繁,如缓存键值对查询 各类查询(等值、范围 ),是 MySQL 默认索引结构
    索引大小 一般较小(存哈希值 ) 相对较大(存索引键和指针等 )
    数据插入更新 哈希冲突会影响性能,插入更新可能需调整哈希表结构 插入更新需维护树平衡,但算法成熟,影响可控

7. spring ioc aop 原理,循环依赖怎么解决,三级缓存具体实现,如果构造函数内存在循环依赖还能解决吗?

  • Spring IOC(控制反转 )
    • 原理:把对象的创建、依赖管理等权力从开发者手中转移到 Spring 容器,容器通过读取配置(XML 、注解等 ),创建 Bean 实例并管理它们的依赖注入。核心是 BeanFactory 和 ApplicationContext ,BeanFactory 是基础容器,ApplicationContext 是其高级实现,包含更多功能。Bean 的创建过程包括实例化(构造对象 )、属性注入(填充依赖 )、初始化(执行 InitializingBean 接口或自定义初始化方法 )等阶段 。
  • Spring AOP(面向切面编程 )
    • 原理:基于动态代理实现,对目标对象生成代理对象,在方法调用前后等时机插入增强逻辑(如日志、事务 )。有 JDK 动态代理(基于接口 ,目标类实现接口时用 )和 CGLIB 动态代理(基于子类,目标类无接口时用 )。通过切点(Pointcut ,定义哪些方法会被增强 )、通知(Advice ,增强的逻辑 )、切面(Aspect ,切点 + 通知 )、织入(Weaving ,将切面应用到目标对象的过程 )来实现 。
  • 循环依赖解决
    • Spring 利用三级缓存解决单例 Bean 的循环依赖(默认单例 Bean 才会处理循环依赖 ):
      • 一级缓存(singletonObjects ):存储完全初始化好的 Bean 。
      • 二级缓存(earlySingletonObjects ):存储早期暴露的 Bean (已实例化但未完成属性注入、初始化 )。
      • 三级缓存(singletonFactories ):存储 Bean 工厂,用于创建早期 Bean 代理(若有 AOP 增强,提前生成代理对象 )。
      • 流程:创建 Bean 时,先从缓存找,没有则创建 Bean 实例,放入三级缓存,然后进行属性注入,若依赖其他 Bean ,递归创建依赖的 Bean ,依赖的 Bean 创建时也会走同样流程,当循环依赖出现时,通过三级缓存提前暴露未完全初始化的 Bean ,让依赖方先引用,最终完成各自的属性注入和初始化,解决循环依赖 。
    • 构造函数循环依赖:无法解决,因为构造函数注入时,对象还未实例化(处于实例化阶段,还没到属性注入 ),三级缓存是在实例化后、属性注入前起作用,所以构造函数循环依赖会抛 BeanCurrentlyInCreationException 异常 。

8. mybatis 相关,#和$怎么区分使用

  • # 占位符
    • 会将传入的参数进行预编译处理,替换为 ? ,作为 PreparedStatement 的参数,能有效防止 SQL 注入(因为参数会被转义 ),是安全的方式,也是 MyBatis 中推荐优先使用的方式。比如 select * from user where id = #{id} ,传入 id1 or 1=1 ,会被处理成 select * from user where id = ? ,参数按字符串等类型传入,不会改变 SQL 结构 。
  • $ 拼接符
    • 直接将参数拼接到 SQL 语句中,不会进行预编译,有 SQL 注入风险,一般用于动态传入表名、列名等场景(这些无法用 # 处理,因为 # 会加引号变成字符串 )。比如动态排序 select * from user order by ${column} ,传入 columnname ,就会拼接成 select * from user order by name ,但使用时要确保参数安全(如由开发者严格控制,不是用户输入 )。

9. 线程池相关,流程,拒绝策略的选择,如何设计线程池最大线程数和核心线程数

  • 线程池流程
    当提交任务到线程池,线程池先判断核心线程是否都在工作,若核心线程有空,用核心线程执行任务;若核心线程都忙,看工作队列是否满,没满则任务入队;队列满了,再判断最大线程数是否已达,没达到则创建新线程(非核心线程 )执行任务;若最大线程数也到了,触发拒绝策略处理任务 。
  • 拒绝策略
    • AbortPolicy(默认 ):直接抛 RejectedExecutionException 异常,阻止任务提交 。
    • CallerRunsPolicy:调用者线程(提交任务的线程 )自己执行任务,减缓任务提交速度,起到限流作用 。
    • DiscardPolicy:直接丢弃任务,不抛异常 。
    • DiscardOldestPolicy:丢弃工作队列中最旧的任务(队首 ),然后尝试提交当前任务 。
      选择依据:业务对任务丢失的容忍度、是否需告警、系统负载情况等。比如业务不允许任务丢失,选 AbortPolicy 并捕获异常处理;希望尽量执行任务,选 CallerRunsPolicy ;对任务及时性要求不高,可丢弃的,选 DiscardPolicyDiscardOldestPolicy
  • 线程池参数设计(核心线程数 corePoolSize 、最大线程数 maximumPoolSize
    • CPU 密集型任务:任务主要消耗 CPU 计算资源,核心线程数和最大线程数一般设为 CPU 核心数 + 1(+1 是为应对偶尔的线程阻塞,如页错误 ),减少线程切换开销 。
    • IO 密集型任务:任务涉及大量 IO 等待(如读写文件、网络请求 ),线程空闲时间多,可适当增大线程数。公式参考 CPU 核心数 * 2 ,或更细致的 CPU 核心数 / (1 - 阻塞系数)(阻塞系数是线程因 IO 等阻塞的时间比例,如 0.9 ,则 CPU 核心数 / 0.1 = 10 * CPU 核心数 ),通过压测调整找到合适值 。
    • 混合任务:拆分任务类型,分别用不同线程池处理,或根据实际任务中 CPU 和 IO 操作占比,结合上述思路调整 。

10. HashMap ConcurrentHashMap

  • 区别
    对比项 HashMap ConcurrentHashMap
    线程安全 非线程安全,多线程下易出现死循环(JDK7 扩容 )、数据
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐