第一轮面试 面试官:首先问你几个基础问题。Java 中 ArrayList 和 LinkedList 的区别是什么? 王铁牛:ArrayList 是基于数组实现的,随机访问快,增删慢,尤其是在中间位置增删。LinkedList 是基于链表实现,增删快,随机访问慢。 面试官:回答得不错。那 HashMap 在 JDK1.7 和 JDK1.8 中有什么主要区别? 王铁牛:1.8 中引入了红黑树,当链表长度达到阈值会转为红黑树,提高查找效率。1.7 只有链表。 面试官:很好。接着,Spring 框架中 IOC 是什么? 王铁牛:IOC 就是控制反转,把对象创建和管理的控制权交给 Spring 容器,这样代码耦合度降低。
第二轮面试 面试官:现在问些稍复杂点的。多线程中线程池的核心参数有哪些,分别有什么作用? 王铁牛:嗯……有核心线程数,好像是一开始就创建的线程数量,还有最大线程数,就是最多能创建的线程数。其他的……不太记得了。 面试官:那 JVM 的内存模型了解吗,简单讲讲。 王铁牛:JVM 内存模型……有堆、栈,堆是存放对象的,栈是存放局部变量这些的。 面试官:好,MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入。
第三轮面试 面试官:最后一轮,问几个更深入的。Dubbo 服务调用的流程是怎样的? 王铁牛:嗯……好像是服务提供者注册服务,然后消费者去调用,中间可能有注册中心啥的,具体不太清楚。 面试官:RabbitMQ 如何保证消息的可靠性? 王铁牛:嗯……可以开启确认机制,生产者发消息后等确认。还有……不太确定了。 面试官:xxl - job 任务调度框架的原理能讲讲吗? 王铁牛:这个……好像是有个调度中心,然后去调度任务,具体细节不太明白。
面试结束,面试官严肃地说:“今天的面试就到这里,你回去等通知吧。我们会综合评估所有候选人,有结果会尽快联系你。感谢你今天来参加面试。”
问题答案:
- ArrayList 和 LinkedList 的区别:
- 数据结构:ArrayList 基于动态数组,内存连续;LinkedList 基于双向链表,内存不连续。
- 随机访问:ArrayList 支持随机访问,通过索引直接定位元素,时间复杂度为 O(1);LinkedList 随机访问慢,需从头或尾遍历,时间复杂度为 O(n)。
- 增删操作:ArrayList 在中间或开头增删元素时,需移动大量元素,时间复杂度为 O(n);LinkedList 增删只需修改指针,时间复杂度为 O(1),但如果要先定位元素,时间复杂度也会变为 O(n)。
- HashMap 在 JDK1.7 和 JDK1.8 的区别:
- 数据结构:JDK1.7 采用数组 + 链表结构;JDK1.8 采用数组 + 链表 + 红黑树结构。当链表长度达到阈值(默认为 8)且数组长度大于 64 时,链表会转为红黑树,提高查找效率,查找时间复杂度从 O(n) 降为 O(logn)。
- 插入方式:JDK1.7 采用头插法,新元素插入链表头部;JDK1.8 采用尾插法,新元素插入链表尾部,避免多线程环境下头插法可能产生的环形链表问题。
- 扩容机制:JDK1.7 扩容时,需重新计算每个元素的哈希值并重新插入新数组;JDK1.8 优化了扩容机制,部分元素在扩容时位置不变,减少了重新计算哈希值和插入的开销。
- Spring 框架中 IOC:
- 概念:控制反转(Inversion of Control),将对象的创建和管理控制权从应用程序代码转移到 Spring 容器。传统开发中,对象由应用程序自己创建和管理,耦合度高;IOC 模式下,Spring 容器负责创建、配置和管理对象(Bean)。
- 实现方式:通过依赖注入(Dependency Injection,DI)实现,常见的注入方式有构造函数注入、Setter 方法注入和接口注入。例如构造函数注入,在类的构造函数中传入依赖对象;Setter 方法注入,通过类的 Setter 方法设置依赖对象。
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池初始化时创建的线程数量,这些线程会一直存活,即使处于空闲状态,除非设置了 allowCoreThreadTimeOut 为 true。
- 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。当任务队列已满且活动线程数小于最大线程数时,线程池会创建新线程处理任务。
- 队列容量(workQueue):用于存放等待执行任务的队列。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列,不存储任务,直接交给线程处理)等。
- 线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,空闲线程会被销毁。
- 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- JVM 内存模型:
- 堆(Heap):是 JVM 中最大的一块内存区域,被所有线程共享,用于存放对象实例和数组。堆可细分为新生代(Young Generation)和老年代(Old Generation),新生代又分为 Eden 区和两个 Survivor 区(S0 和 S1)。对象通常在 Eden 区创建,经过多次垃圾回收后,如果对象存活,会被移动到老年代。
- 栈(Stack):每个线程都有自己的栈,用于存放局部变量表、操作数栈、动态链接、方法出口等信息。栈的生命周期与线程相同,线程结束,栈也随之销毁。栈帧随着方法的调用和返回进行入栈和出栈操作。
- 方法区(Method Area):被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK8 及以后,方法区被元空间(Metaspace)取代,元空间使用本地内存。
- 程序计数器(Program Counter Register):每个线程都有一个程序计数器,是一块较小的内存空间,用于记录当前线程执行的字节码指令地址。如果执行的是 native 方法,程序计数器值为空。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 的 set 方法赋值,这样可以有效防止 SQL 注入。例如:
SELECT * FROM user WHERE username = #{username},实际执行时会变为SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 设置参数值。 - **{} 时,会直接将 {username}
,如果 username 变量值为 'admin',实际执行的 SQL 就是SELECT * FROM user WHERE username = admin`,这种方式容易导致 SQL 注入,一般用于传入数据库对象,如表名等。
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 的 set 方法赋值,这样可以有效防止 SQL 注入。例如:
- Dubbo 服务调用流程:
- 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如 Zookeeper)。注册中心保存服务提供者的地址、端口等信息。
- 服务订阅:服务消费者启动时,向注册中心订阅自己所需的服务。注册中心返回服务提供者的地址列表给消费者。
- 服务调用:消费者从地址列表中选择一个服务提供者进行调用。调用过程中,Dubbo 会进行负载均衡(如随机、轮询等策略)选择具体的服务实例。如果服务提供者出现故障,Dubbo 会自动将其从地址列表中移除,实现服务的高可用。
- RabbitMQ 保证消息可靠性:
- 生产者确认机制(publisher confirm):生产者发送消息后,RabbitMQ 会给生产者发送确认消息,告知消息是否成功接收。生产者可以通过监听确认消息来判断消息是否发送成功。有两种确认模式:普通确认模式(channel.waitForConfirms()),发送一条消息等待确认;批量确认模式(channel.waitForConfirmsOrDie()),发送一批消息等待确认;异步确认模式(addConfirmListener()),通过回调函数处理确认结果。
- 持久化:
- 队列持久化:声明队列时设置 durable 为 true,这样 RabbitMQ 重启后队列依然存在。
- 消息持久化:发送消息时设置 MessageProperties.PERSISTENT_TEXT_PLAIN,消息会被持久化到磁盘,即使 RabbitMQ 重启,消息也不会丢失。
- 消费者确认机制(ack):消费者接收消息后,需要向 RabbitMQ 发送确认消息(ack),告知 RabbitMQ 消息已成功处理。如果消费者未发送 ack 且设置了 autoAck 为 false,RabbitMQ 会认为消息未被成功处理,会重新将消息发送给其他消费者或在一定时间后重新发送给该消费者。
- xxl - job 任务调度框架原理:
- 调度中心:是整个调度系统的核心,负责任务的管理、调度触发、监控等。调度中心基于数据库存储任务信息、调度日志等数据。
- 执行器:部署在业务系统中,负责接收调度中心的调度请求并执行任务。执行器与调度中心通过网络通信,采用 HTTP 协议进行交互。
- 任务注册:执行器启动时,会向调度中心注册自己,并汇报自己能执行的任务列表。调度中心记录执行器的地址、端口等信息。
- 任务调度:调度中心根据任务的调度规则(如 cron 表达式),定时触发任务调度。调度中心向对应的执行器发送调度请求,执行器接收到请求后,执行具体的任务逻辑。
- 任务结果反馈:执行器执行任务后,将任务执行结果反馈给调度中心。调度中心记录任务执行日志,方便用户查看任务执行情况。