第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会创建一个新的数组,新数组容量是原数组的 1.5 倍,然后把原数组的元素复制到新数组。 面试官:很好。HashMap 在 JDK1.8 中引入红黑树,为什么要引入呢? 王铁牛:好像是为了提高查找效率,链表太长查找就慢,红黑树查找快。
第二轮面试 面试官:接下来聊聊多线程和线程池。多线程编程中,线程安全问题是怎么产生的? 王铁牛:多个线程同时访问共享资源,就可能出现线程安全问题。 面试官:那线程池的核心参数有哪些,分别有什么作用? 王铁牛:有核心线程数、最大线程数、队列容量,核心线程数就是一直保留的线程数,最大线程数是能创建的最大线程数,队列容量是存放任务的队列大小。 面试官:那线程池在什么情况下会拒绝任务? 王铁牛:嗯……当队列满了,而且线程数达到最大线程数,就会拒绝任务。
第三轮面试 面试官:谈谈 Spring 和 Spring Boot。Spring 中的 IOC 是什么,有什么作用? 王铁牛:IOC 就是控制反转,把对象的创建和管理交给 Spring 容器,这样代码耦合度更低。 面试官:Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 能快速搭建项目,自动配置很多东西,开发更方便。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 更安全,能防止 SQL 注入。 面试官:Dubbo 是什么,在微服务架构中有什么作用? 王铁牛:Dubbo 是个分布式服务框架,能实现服务治理,像服务注册、发现这些。 面试官:RabbitMQ 有哪些常见的应用场景? 王铁牛:可以用于异步处理,比如订单下单后发消息通知其他系统处理后续操作;还有解耦,不同系统通过消息队列通信。 面试官:xxl - job 是什么,它解决了什么问题? 王铁牛:它是个分布式任务调度平台,能解决分布式系统中任务调度的问题。 面试官:Redis 有哪些数据类型,分别适用于什么场景? 王铁牛:有字符串、哈希、列表、集合、有序集合。字符串存简单数据,哈希存对象,列表做队列,集合去重,有序集合可以排序。
面试总结:从这轮面试来看,你对于一些基础的知识点掌握得还不错,像 ArrayList、HashMap 的底层结构,多线程的基础概念,以及 Spring、Spring Boot、MyBatis 的一些基本特性都能回答上来。但在一些稍微深入的问题上,比如线程池拒绝任务的具体场景分析,还可以回答得更完善。对于 Dubbo、RabbitMQ、xxl - job、Redis 这些技术,虽然能说出大概用途,但在细节和实际应用场景的深入理解上还有所欠缺。回去等通知吧,我们会综合评估所有候选人后,再做决定。
答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容。数组的特点是可以根据索引快速访问元素,所以 ArrayList 支持快速随机访问。
- HashMap:JDK1.8 之前,底层是数组 + 链表结构。数组的每个位置是一个链表的头节点。当发生哈希冲突时,新的元素会以链表的形式挂在对应数组位置的链表上。JDK1.8 之后,当链表长度大于 8 且数组容量大于 64 时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,查找、插入、删除的时间复杂度为 O(log n),相比链表的 O(n) 查找效率更高。
- ArrayList 扩容操作:
- ArrayList 有一个容量的概念,当向 ArrayList 中添加元素时,如果当前元素个数达到了容量阈值(一般是当前容量),就会触发扩容。
- 扩容时,会创建一个新的数组,新数组的容量是原数组容量的 1.5 倍(原容量右移一位再加原容量)。
- 然后通过 System.arraycopy 方法将原数组的元素复制到新数组中。这样就完成了扩容操作,使得 ArrayList 可以继续添加新元素。
- HashMap 引入红黑树的原因:
- 在 JDK1.8 之前,HashMap 处理哈希冲突是通过链表。当哈希冲突严重时,链表会变得很长,此时查找元素的时间复杂度会退化为 O(n),效率很低。
- 引入红黑树后,当链表长度大于 8 且数组容量大于 64 时,链表会转化为红黑树。红黑树的查找、插入、删除操作平均时间复杂度为 O(log n),大大提高了在哈希冲突较多情况下的查找效率。
- 多线程线程安全问题产生原因:
- 当多个线程同时访问共享资源,并且至少有一个线程对共享资源进行写操作时,就可能出现线程安全问题。
- 例如,多个线程同时对一个共享变量进行自增操作。由于 CPU 时间片的调度,可能一个线程读取了共享变量的值,还没来得及写回,另一个线程又读取了相同的值,这样就会导致最终结果比预期的少。这就是典型的线程安全问题,也叫竞态条件。
- 线程池核心参数及作用:
- 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。当有新任务提交时,如果当前线程数小于 corePoolSize,会创建新的线程来处理任务。
- 最大线程数(maximumPoolSize):线程池中允许创建的最大线程数。当任务队列已满,且当前线程数小于 maximumPoolSize 时,会创建新的线程来处理任务。
- 队列容量(workQueue):用于存放等待处理任务的队列。当线程数达到 corePoolSize 后,新的任务会被放入队列中等待处理。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列,实际使用时也可设置为有界)、SynchronousQueue(同步队列,不存储任务,直接提交给线程处理)等。
- 线程池拒绝任务的情况:
- 当任务队列已满(workQueue 达到其容量上限),并且当前线程数已经达到最大线程数(maximumPoolSize)时,新提交的任务就会被拒绝。
- 线程池提供了几种拒绝策略,如 AbortPolicy(默认策略,直接抛出 RejectedExecutionException 异常)、CallerRunsPolicy(将任务交给调用者线程来执行)、DiscardPolicy(直接丢弃任务,不做任何处理)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
- Spring 中 IOC(控制反转):
- 概念:IOC 即控制反转,是 Spring 框架的核心思想之一。它将对象的创建和管理从应用程序代码中转移到 Spring 容器中。在传统的编程中,对象的创建和依赖关系的管理由应用程序自己负责,这会导致代码耦合度较高。而在 Spring 中,通过 IOC,对象的创建、初始化、销毁等工作都由 Spring 容器来完成,应用程序只需要使用这些对象即可。
- 作用:降低代码耦合度,提高代码的可维护性和可测试性。例如,一个类 A 依赖类 B,如果在类 A 中自己创建类 B 的实例,那么类 A 与类 B 就紧密耦合在一起。当类 B 的实现发生变化时,类 A 也需要修改。而使用 IOC,类 A 只需要声明对类 B 的依赖,由 Spring 容器来注入类 B 的实例,这样类 A 与类 B 的耦合度就大大降低。
- Spring Boot 相对于 Spring 的优势:
- 快速搭建项目:Spring Boot 提供了大量的 Starter 依赖,通过简单的配置就能快速搭建一个基于 Spring 的项目,减少了繁琐的配置过程。例如,要搭建一个 Spring Boot Web 项目,只需要引入 spring - boot - starter - web 依赖,Spring Boot 会自动配置好 Web 开发所需的各种组件,如 Tomcat 服务器、Spring MVC 等。
- 自动配置:Spring Boot 能根据项目的依赖自动配置 Spring 应用。它会根据类路径下是否存在某些类,来判断是否需要配置相应的功能。比如,当项目中引入了 MySQL 驱动依赖,Spring Boot 会自动配置好数据源、JdbcTemplate 等相关组件,开发者无需手动编写大量的配置文件。
- 生产级别的监控:Spring Boot Actuator 提供了生产级别的监控功能,如查看应用的健康状态、性能指标等。可以通过 HTTP 端点暴露应用的运行时信息,方便运维和监控。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 来执行 SQL。这种方式可以有效防止 SQL 注入攻击,因为参数会被当作字符串处理,而不是直接嵌入 SQL 语句中。例如,SQL 语句为 “SELECT * FROM user WHERE username = #{username}”,实际执行时会变为 “SELECT * FROM user WHERE username =?”,然后通过 PreparedStatement 的 setString 方法设置参数值。
- **{} 时,会直接将 {username}”,如果用户输入 “admin OR 1 = 1”,那么实际执行的 SQL 就变为 “SELECT * FROM user WHERE username = admin OR 1 = 1”,这会导致非法获取数据。所以在使用 ${} 时,要确保参数值是安全的,一般用于传入表名、列名等。
- Dubbo 在微服务架构中的作用:
- 服务注册与发现:Dubbo 提供了服务注册中心(如 Zookeeper、Nacos 等),服务提供者将自己的服务注册到注册中心,服务消费者从注册中心获取服务提供者的地址信息。这样,服务消费者就不需要硬编码服务提供者的地址,实现了服务的动态发现。
- 负载均衡:当有多个服务提供者提供相同的服务时,Dubbo 可以实现负载均衡,将请求均匀地分配到各个服务提供者上,提高系统的整体性能和可用性。常见的负载均衡策略有随机、轮询、最少活跃调用数等。
- 服务治理:Dubbo 支持对服务进行治理,如服务的版本控制、流量控制、容错处理等。可以通过配置不同的规则,对服务的调用进行精细化管理,保证微服务架构的稳定运行。
- RabbitMQ 常见应用场景:
- 异步处理:例如在电商系统中,用户下单后,可能需要发送短信通知、更新库存、记录日志等一系列操作。如果这些操作都同步执行,会导致用户等待时间过长。可以将这些操作封装成消息发送到 RabbitMQ 队列中,下单操作完成后,由其他消费者异步处理这些消息,提高系统的响应速度。
- 解耦:不同的系统之间通过 RabbitMQ 进行通信,降低系统之间的耦合度。比如订单系统和物流系统,订单系统只需要将订单信息发送到 RabbitMQ 队列,物流系统从队列中获取订单信息进行处理,两个系统不需要直接交互,当其中一个系统发生变化时,对另一个系统的影响较小。
- 流量削峰:在高并发场景下,如秒杀活动,大量的请求同时涌入系统。可以将这些请求先发送到 RabbitMQ 队列中,系统按照一定的速度从队列中获取请求进行处理,避免瞬间高并发对系统造成过大压力,起到流量削峰的作用。
- xxl - job 分布式任务调度平台:
- 概念:xxl - job 是一个轻量级分布式任务调度平台,它提供了任务的调度、执行、监控等功能。
- 解决的问题:在分布式系统中,任务调度变得复杂。传统的单机任务调度框架(如 Quartz)无法满足分布式环境下的需求。xxl - job 可以实现分布式任务的统一管理,包括任务的新增、修改、删除、暂停、恢复等操作。它支持任务的分片执行,将一个大任务拆分成多个小任务在不同的机器上并行执行,提高任务执行效率。同时,还提供了任务执行日志查看、任务失败重试等功能,方便对任务进行监控和管理。
- Redis 数据类型及适用场景:
- 字符串(String):最基本的数据类型,可以存储任何类型的数据,如数字、文本、二进制数据等。适用于存储简单的键值对,例如缓存用户信息、配置参数等。例如,可以将用户的登录名和密码以键值对的形式存储在 Redis 中,键为用户 ID,值为包含登录名和密码的 JSON 字符串。
- 哈希(Hash):用于存储对象,它将对象的每个字段和值存储为一个键值对。适用于存储和操作对象,比如存储用户的详细信息,每个字段如姓名、年龄、地址等作为哈希的字段,对应的值作为哈希的值。这样可以方便地对对象的单个字段进行操作,而不需要获取整个对象。
- 列表(List):是一个链表结构,可以从链表的两端进行插入和删除操作。适用于实现队列和栈的功能,例如消息队列,生产者将消息从列表的一端插入,消费者从另一端取出消息。也可以用于记录日志,按照时间顺序将日志记录插入到列表中。
- 集合(Set):无序的、不重复的元素集合。适用于去重场景,比如统计网站的独立访客,将每个访客的 IP 地址作为集合的元素,由于集合的元素唯一性,就可以自动去重。还可以用于实现交集、并集、差集等集合运算,例如计算两个用户群体的共同兴趣爱好。
- 有序集合(Sorted Set):与集合类似,但每个元素都关联一个分数(score),根据分数对元素进行排序。适用于需要排序的场景,如排行榜,将用户的分数作为 score,用户 ID 作为元素,就可以根据分数对用户进行排名。