第一轮面试 面试官:先问你几个基础问题。Java 中 ArrayList 和 LinkedList 的区别是什么?在实际业务场景中,什么情况下会优先选择 ArrayList ? 王铁牛:ArrayList 是基于数组实现的,LinkedList 是基于链表实现的。在需要频繁随机访问的场景下,优先选 ArrayList ,因为数组可以通过索引快速定位元素。 面试官:不错,回答得很清晰。那 HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 之后引入了红黑树,当链表长度达到阈值时会转为红黑树,提高查找效率,1.7 只有链表。 面试官:很好。Spring 框架中 IOC 和 AOP 分别是什么? 王铁牛:IOC 是控制反转,把对象创建和管理的控制权交给 Spring 容器。AOP 是面向切面编程,在不修改原有代码的基础上增加功能,像日志记录、事务管理这些。 面试官:回答得不错,基础还是挺扎实的。
第二轮面试 面试官:接下来问些更深入的。多线程环境下,如何保证线程安全? 王铁牛:可以用 synchronized 关键字,还有 Lock 接口。 面试官:那说说 synchronized 的原理呢? 王铁牛:嗯……它好像是对对象加锁,让同一时间只有一个线程能访问。 面试官:回答得不太清晰,synchronized 是基于对象头的 Mark Word 来实现锁机制的。那线程池的核心参数有哪些,分别代表什么含义? 王铁牛:有核心线程数、最大线程数,还有队列容量。核心线程数就是一开始创建的线程数,最大线程数就是最多能创建的线程数,队列容量就是存放任务的队列大小。 面试官:基本意思对,但表述可以更准确些。
第三轮面试 面试官:现在问些和框架、中间件相关的。Spring Boot 自动配置的原理是什么? 王铁牛:它好像是根据类路径下的依赖,自动配置一些组件。 面试官:原理阐述得不够详细。MyBatis 中 #{} 和 {} 的区别是什么?在 SQL 注入防范上有什么不同? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是字符串替换,可能导致 SQL 注入。 面试官:回答得还行。Dubbo 服务调用的流程是怎样的? 王铁牛:嗯……就是服务提供者暴露服务,服务消费者引用服务,然后通过注册中心进行服务发现。 面试官:流程说得比较简略。RabbitMQ 如何保证消息不丢失? 王铁牛:可以开启持久化。 面试官:还有其他方面呢?回答得不够全面。Xxl - job 任务调度的核心原理是什么? 王铁牛:不太清楚。 面试官:Redis 缓存雪崩、缓存穿透、缓存击穿分别是什么,怎么解决? 王铁牛:缓存雪崩好像是大量缓存同时过期,解决办法可以设置不同过期时间。缓存穿透不太清楚,缓存击穿好像是一个 key 过期,大量请求过来,解决办法可以加互斥锁。 面试官:部分回答正确,但有些概念理解还不够深入。今天的面试就到这里,你回去等通知吧。我们会综合评估所有候选人,有结果会尽快联系你。感谢你今天来参加面试。
问题答案:
- ArrayList 和 LinkedList 的区别及适用场景:
- 区别:
- 数据结构:ArrayList 基于数组,内存连续;LinkedList 基于双向链表,内存不连续。
- 随机访问:ArrayList 支持快速随机访问,通过索引直接定位元素,时间复杂度为 O(1);LinkedList 随机访问慢,需从头或尾遍历,时间复杂度为 O(n)。
- 插入和删除:ArrayList 在中间插入或删除元素时,需移动大量元素,时间复杂度为 O(n);LinkedList 在中间插入或删除元素只需修改指针,时间复杂度为 O(1),但在链表头部或尾部操作时,两者效率相近。
- 适用场景:
- ArrayList:适用于频繁随机访问的场景,如数据查询。因为数组结构能快速定位元素,提高查询效率。
- LinkedList:适用于频繁插入和删除操作的场景,如实时消息处理,每次有新消息到来可快速插入链表。
- 区别:
- HashMap 在 JDK1.7 和 JDK1.8 的主要区别:
- 数据结构:
- JDK1.7:采用数组 + 链表结构。
- JDK1.8:采用数组 + 链表 + 红黑树结构。当链表长度达到阈值(默认为 8)且数组长度大于等于 64 时,链表会转为红黑树,以提高查找效率。
- 哈希冲突解决方式:
- JDK1.7:主要采用头插法解决哈希冲突,在多线程环境下可能形成环形链表,导致死循环。
- JDK1.8:采用尾插法,避免了环形链表问题。同时引入红黑树优化,当链表过长时,查找时间复杂度从 O(n) 降为 O(logn)。
- 数据结构:
- Spring 框架中 IOC 和 AOP:
- IOC(控制反转):
- 概念:将对象的创建和管理控制权从应用程序代码转移到 Spring 容器。应用程序不再自己创建对象,而是由 Spring 容器负责创建、配置和管理对象及其依赖关系。
- 实现方式:通过依赖注入(DI)实现,主要有构造函数注入、Setter 方法注入和接口注入(较少使用)。例如,一个类 A 依赖类 B ,在传统方式中 A 需自己创建 B 的实例,而在 Spring 中,Spring 容器创建 B 的实例并注入到 A 中。
- AOP(面向切面编程):
- 概念:将与业务逻辑无关但又贯穿多个业务模块的功能(如日志记录、事务管理、权限控制等)抽取出来,形成一个切面,在不修改原有业务代码的基础上,将这些功能动态地添加到目标业务模块中。
- 实现方式:Spring AOP 基于动态代理实现,有 JDK 动态代理(基于接口)和 CGLIB 动态代理(基于类)。例如,在方法执行前记录日志,可通过 AOP 配置切点(目标方法)和通知(记录日志的逻辑)来实现。
- IOC(控制反转):
- 多线程环境下保证线程安全的方式:
- synchronized 关键字:
- 原理:基于对象头的 Mark Word 实现锁机制。当线程进入同步块时,会在对象头的 Mark Word 中记录锁信息,根据锁的状态(无锁、偏向锁、轻量级锁、重量级锁)进行不同处理。例如,偏向锁是在只有一个线程访问同步块时,将锁偏向该线程,减少锁竞争开销;轻量级锁是在多个线程交替访问同步块时,通过 CAS 操作尝试获取锁;重量级锁则是在竞争激烈时,通过操作系统互斥量实现线程阻塞和唤醒。
- 使用方式:可修饰方法(实例方法锁对象为 this ,静态方法锁对象为类对象)或代码块(锁对象自定义)。
- Lock 接口:
- 原理:基于 AQS(AbstractQueuedSynchronizer)框架实现。AQS 维护一个 FIFO 队列,用于存放等待获取锁的线程。例如,ReentrantLock 是 Lock 接口的实现类,通过 AQS 实现可重入锁机制。
- 使用方式:通过 lock() 方法获取锁,unlock() 方法释放锁,相比 synchronized 更灵活,可实现公平锁、非公平锁,还可中断等待锁的线程等。
- synchronized 关键字:
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池在没有任务时,会保持的线程数量。即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true 。例如,一个 Web 服务器线程池,可设置核心线程数为 10 ,保证始终有 10 个线程随时处理请求。
- 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量。当任务队列已满且核心线程都在忙碌时,线程池会创建新线程,直到达到最大线程数。但如果任务数继续增加,超过最大线程数和队列容量,就会触发拒绝策略。
- 队列容量(workQueue):用于存放等待执行任务的队列。常见队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列,默认容量为 Integer.MAX_VALUE )、SynchronousQueue(不存储任务,直接提交给线程处理)等。例如,使用 ArrayBlockingQueue(100) 表示队列最多存放 100 个任务。
- 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。超过这个时间,空闲线程会被销毁。例如,设置存活时间为 60 秒,意味着 60 秒内没有新任务,多余线程将被销毁。
- 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS 表示秒。
- 拒绝策略(RejectedExecutionHandler):当任务无法提交到队列且线程数达到最大线程数时,线程池采取的策略。常见策略有 AbortPolicy(抛出异常)、CallerRunsPolicy(将任务返回给调用者执行)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,尝试提交新任务)。
- Spring Boot 自动配置原理:
- 核心机制:基于 Spring 4.0 引入的条件化配置(@Conditional 注解)。Spring Boot 启动时,会扫描 META - INF/spring.factories 文件,该文件中定义了各种自动配置类。
- 流程:
- 启动扫描:Spring Boot 启动类标注了 @SpringBootApplication ,该注解包含 @EnableAutoConfiguration ,会触发自动配置。
- 条件判断:自动配置类上使用 @Conditional 及其衍生注解(如 @ConditionalOnClass 、@ConditionalOnProperty 等)。例如,@ConditionalOnClass 表示当类路径下存在某个类时,才进行该配置。如 ThymeleafAutoConfiguration 类上有 @ConditionalOnClass(Thymeleaf.class) ,只有当 Thymeleaf 相关依赖在类路径下时,才会配置 Thymeleaf 相关功能。
- 配置生效:满足条件的自动配置类会被加载,将相关组件注册到 Spring 容器中,完成自动配置。
- MyBatis 中 #{} 和 ${} 的区别及 SQL 注入防范:
- 区别:
- #{}:预编译方式,将传入的值当作一个参数,MyBatis 会在 SQL 语句中使用占位符(?),然后将参数值设置到占位符处。例如,SQL 语句为 “SELECT * FROM user WHERE username = #{username}” ,实际执行时会替换为 “SELECT * FROM user WHERE username =?” ,并设置参数值。
- **{username}” ,如果传入值为 “admin' OR '1'='1” ,则实际执行的 SQL 为 “SELECT * FROM user WHERE username = admin' OR '1'='1” ,会导致 SQL 注入。
- SQL 注入防范:
- #{}:由于是预编译,数据库会对参数值进行转义处理,能有效防止 SQL 注入。
- **{} 时,如果传入值来自用户输入且未经过严格校验和过滤,容易导致 SQL 注入。只有在确定传入值是安全的情况下(如传入表名、字段名等),才使用 ${} 。
- 区别:
- Dubbo 服务调用流程:
- 服务暴露:
- 服务提供者启动:加载 Spring 配置文件,初始化服务实例。
- 注册服务:服务提供者将服务接口和实现类等信息注册到注册中心(如 Zookeeper )。注册中心维护服务提供者列表。
- 服务引用:
- 服务消费者启动:加载 Spring 配置文件,从注册中心订阅所需服务。
- 获取服务列表:注册中心将服务提供者列表返回给服务消费者。
- 服务调用:
- 负载均衡:服务消费者根据负载均衡策略(如随机、轮询、权重等)从服务提供者列表中选择一个服务提供者。
- 远程调用:通过网络通信(如 Netty )与服务提供者建立连接,发送调用请求,服务提供者接收请求并执行服务方法,将结果返回给服务消费者。
- 服务暴露:
- RabbitMQ 保证消息不丢失的方法:
- 生产者端:
- 开启确认机制(publisher confirm):生产者发送消息后,RabbitMQ 会返回确认消息给生产者,告知消息是否成功到达 Broker 。可通过设置 channel.confirmSelect() 开启,生产者通过监听 ConfirmListener 来处理确认结果。
- 事务机制:生产者通过 channel.txSelect() 开启事务,发送消息后通过 channel.txCommit() 提交事务,若发送失败可通过 channel.txRollback() 回滚。但事务机制会严重影响性能,不推荐高并发场景使用。
- Broker 端:
- 消息持久化:将队列和消息都设置为持久化。队列持久化通过声明队列时设置 durable = true ,消息持久化通过设置 MessageProperties.PERSISTENT_TEXT_PLAIN 。这样即使 RabbitMQ 服务器重启,队列和消息也不会丢失。
- 消费者端:
- 手动确认(manual ack):消费者设置 autoAck = false ,在处理完消息后,通过 channel.basicAck(deliveryTag, false) 手动确认消息已被成功处理。若处理过程中出现异常,可通过 channel.basicNack(deliveryTag, false, true) 拒绝消息,让 RabbitMQ 重新发送给其他消费者或重新入队。
- 生产者端:
- Xxl - job 任务调度核心原理:
- 调度中心:
- 任务管理:提供可视化界面,用于管理任务(新增、修改、删除等),配置任务执行参数(如执行器、执行周期等)。
- 调度触发:根据任务配置的执行周期,定时触发任务调度。通过 Quartz 框架实现定时任务调度,将任务封装成 Job ,按照 cron 表达式定义的时间规则触发执行。
- 执行器:
- 注册:执行器启动时,向调度中心注册自身信息(如 IP 、端口、应用名称等)。调度中心维护执行器列表。
- 任务执行:调度中心触发任务调度后,将任务发送给对应的执行器。执行器接收到任务后,通过反射调用任务类的 execute 方法执行具体业务逻辑,并将执行结果返回给调度中心。
- Redis 缓存雪崩、缓存穿透、缓存击穿及解决办法:
- 缓存雪崩:
- 概念:大量缓存数据在同一时间过期,导致大量请求直接访问数据库,使数据库压力骤增,甚至可能导致数据库崩溃。
- 解决办法:
- 设置不同过期时间:对缓存数据设置随机的过期时间,避免大量数据同时过期。例如,原本过期时间为 1 小时,可改为 50 - 70 分钟之间的随机值。
- 使用互斥锁:在缓存失效时,使用互斥锁(如 Redis 的 SETNX 命令)保证只有一个线程去查询数据库并更新缓存,其他线程等待,避免大量请求同时查询数据库。
- 缓存穿透:
- 概念:查询一个不存在的数据,由于缓存中没有,每次都会去查询数据库,若有大量这种请求,会导致数据库压力增大。
- 解决办法:
- 布隆过滤器:在查询前,先通过布隆过滤器判断数据是否存在。布隆过滤器是一种概率型数据结构,通过多个哈希函数将数据映射到一个位数组中,查询时若数据对应的位都为 1 ,则大概率数据存在,否则一定不存在。即使布隆过滤器误判,也能拦截大部分不存在的数据查询。
- 缓存空值:当查询数据库发现数据不存在时,也将空值缓存起来,并设置较短的过期时间,避免后续重复查询数据库。
- 缓存击穿:
- 概念:一个热点 key 过期瞬间,大量请求同时访问,这些请求都会去查询数据库,导致数据库压力瞬间增大。
- 解决办法:
- 加互斥锁:与缓存雪崩中使用互斥锁类似,在热点 key 过期时,只有一个线程能查询数据库并更新缓存,其他线程等待。
- 热点数据不过期:对热点数据不设置过期时间,由后台定时任务或其他机制更新数据,保证数据的实时性。