第一轮面试 面试官:首先问几个基础问题。Java 中 ArrayList 和 LinkedList 的区别是什么?在实际业务场景中,什么情况下更适合用 ArrayList 呢?还有,HashMap 的底层数据结构是怎样的? 王铁牛:ArrayList 是基于数组实现的,LinkedList 是基于链表实现的。在需要频繁随机访问的场景下适合用 ArrayList。HashMap 底层是数组加链表,JDK8 之后引入了红黑树。 面试官:回答得不错。那再说说 ArrayList 在扩容时具体是怎么操作的?HashMap 在发生哈希冲突时,链表转红黑树的条件是什么? 王铁牛:ArrayList 扩容是先创建一个更大的数组,然后把原数组内容复制过去。HashMap 链表长度大于 8 且数组容量大于等于 64 时转红黑树。 面试官:很好。最后一个问题,HashMap 的线程安全性如何?在多线程环境下使用会有什么问题? 王铁牛:HashMap 是非线程安全的,多线程环境下使用可能会导致数据不一致等问题。
第二轮面试 面试官:接下来聊聊多线程和 JUC 相关。讲讲线程的生命周期有哪些状态?在业务中,线程池一般用于什么场景? 王铁牛:线程有新建、就绪、运行、阻塞、死亡这些状态。线程池适用于需要处理大量短时间任务的场景,能提高效率,减少资源开销。 面试官:那线程池的核心参数有哪些?分别代表什么含义? 王铁牛:有核心线程数、最大线程数、队列容量等。核心线程数是线程池初始化的线程数量,最大线程数是线程池能容纳的最大线程数,队列容量是存放任务的队列大小。 面试官:好。说说 ThreadLocal 的作用是什么?在实际业务中如何使用? 王铁牛:ThreadLocal 是让每个线程都有自己独立的变量副本,在一些需要每个线程有独立数据的场景会用到,比如数据库连接。
第三轮面试 面试官:我们谈谈框架相关。Spring 中 IOC 和 AOP 分别是什么?在实际项目里有什么应用场景? 王铁牛:IOC 是控制反转,把对象创建和管理交给 Spring 容器。AOP 是面向切面编程,比如在日志记录、事务管理方面应用。 面试官:那 Spring Boot 相对于 Spring 有什么优势?它是如何实现快速开发的? 王铁牛:Spring Boot 简化了 Spring 配置,能快速搭建项目。通过自动配置等机制实现快速开发。 面试官:最后问下 MyBatis、Dubbo、RabbitMq、xxl - job、Redis 在项目中的作用分别是什么? 王铁牛:MyBatis 是数据库持久层框架,操作数据库方便。Dubbo 是分布式服务框架,用于服务治理。RabbitMq 是消息队列,能实现异步解耦。xxl - job 是分布式任务调度框架。Redis 能做缓存,提高系统性能。
面试官:好的,今天的面试就到这里。你的表现整体有一定基础,但在一些深入问题上还可以再加强理解。回去等我们的通知吧,无论结果如何,我们都会在一周内给你回复。希望你之后能继续深入学习相关知识,提升自己的技术能力。如果这次没有通过,也不要气馁,技术是不断积累的过程。感谢你今天来参加面试。
问题答案:
- ArrayList 和 LinkedList 的区别及 ArrayList 适用场景:
- 区别:
- 底层数据结构:ArrayList 基于数组,LinkedList 基于双向链表。数组在内存中是连续存储的,链表节点在内存中不一定连续。
- 随机访问效率:ArrayList 支持随机访问,通过索引直接定位元素,时间复杂度为 O(1);LinkedList 随机访问效率低,需要从头或尾遍历链表,时间复杂度为 O(n)。
- 插入和删除效率:在 ArrayList 中间插入或删除元素,需要移动大量元素,时间复杂度为 O(n);LinkedList 在中间插入或删除元素只需修改前后节点的指针,时间复杂度为 O(1)。但如果在 ArrayList 尾部插入或删除元素,时间复杂度为 O(1)。
- ArrayList 适用场景:当需要频繁进行随机访问操作,如根据索引获取元素,且插入和删除操作主要在尾部进行时,适合使用 ArrayList。例如在分页查询结果集处理场景中,需要根据页码快速定位数据,ArrayList 比较合适。
- 区别:
- HashMap 底层数据结构:
- JDK7 及之前:底层是数组加链表结构。数组的每个元素是一个链表头节点,当发生哈希冲突时,新元素会以头插法插入到链表中。
- JDK8 及之后:底层依然是数组加链表,但引入了红黑树。当链表长度大于 8 且数组容量大于等于 64 时,链表会转换为红黑树,以提高查找效率。当红黑树节点数量小于 6 时,又会退化为链表。这样设计是为了在哈希冲突较严重时,利用红黑树的平衡特性,将查找时间复杂度从链表的 O(n)优化为红黑树的 O(logn)。
- ArrayList 扩容操作:
- 当 ArrayList 中的元素数量达到容量阈值(容量 * 负载因子,默认负载因子为 0.75)时,会进行扩容。
- 扩容时,先创建一个新的数组,新数组的容量是原数组容量的 1.5 倍(原容量右移一位再加原容量)。
- 然后将原数组中的元素复制到新数组中。这个过程涉及到内存的重新分配和数据复制,所以频繁扩容会影响性能。
- HashMap 链表转红黑树条件:
- 链表长度大于 8:当链表长度达到 8 时,说明哈希冲突比较严重,为了提高查找效率,考虑转换为红黑树。
- 数组容量大于等于 64:如果数组容量较小,即使链表长度达到 8,也不急于转换为红黑树,因为数组扩容可能会重新分配元素位置,减少哈希冲突。只有当数组容量足够大,再转换为红黑树才能更好地发挥其性能优势。
- HashMap 线程安全性及多线程问题:
- 线程安全性:HashMap 是非线程安全的。
- 多线程问题:在多线程环境下,可能会出现数据不一致问题。例如,当多个线程同时进行 put 操作时,可能会导致链表形成环形结构,进而在 get 操作时出现死循环。另外,多个线程同时修改 HashMap 的结构(如扩容),可能会导致数据丢失或其他不可预期的结果。
- 线程的生命周期状态:
- 新建(New):线程对象被创建,但尚未调用 start()方法,此时线程还未开始运行。
- 就绪(Runnable):调用 start()方法后,线程进入就绪状态,等待 CPU 调度执行。处于这个状态的线程可能在就绪队列中等待获取 CPU 资源。
- 运行(Running):线程获取到 CPU 资源,正在执行 run()方法中的代码。
- 阻塞(Blocked):线程由于某些原因暂时无法继续执行,如等待 I/O 操作完成、等待锁等。处于阻塞状态的线程不会占用 CPU 资源。常见的阻塞情况有:
- 等待阻塞:调用 Object 的 wait()方法,线程进入等待状态,直到其他线程调用 notify()或 notifyAll()方法唤醒它。
- 同步阻塞:当线程尝试获取一个被其他线程占用的锁时,会进入同步阻塞状态,直到获取到锁。
- 其他阻塞:如执行 I/O 操作、线程睡眠(调用 Thread.sleep()方法)等。
- 死亡(Dead):线程执行完 run()方法,或者因异常退出,线程生命周期结束。
- 线程池适用场景:
- 大量短时间任务:例如 Web 服务器处理大量用户请求,每个请求处理时间较短,但请求数量巨大。使用线程池可以避免频繁创建和销毁线程的开销,提高系统性能和响应速度。
- 资源管理:通过线程池可以控制并发线程的数量,避免过多线程导致系统资源耗尽。例如在数据库连接池场景中,限制同时访问数据库的线程数量,防止数据库过载。
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池初始化时创建的线程数量,这些线程会一直存活,即使处于空闲状态,除非设置了 allowCoreThreadTimeOut 为 true。
- 最大线程数(maximumPoolSize):线程池能容纳的最大线程数量。当任务队列已满且活动线程数小于最大线程数时,线程池会创建新的线程来处理任务。
- 队列容量(workQueue):用于存放等待处理任务的队列。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列,不存储元素,直接提交任务给线程处理)等。
- 线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,多余的空闲线程在等待新任务到来的时间超过该存活时间后,会被销毁。
- 时间单位(unit):线程存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- ThreadLocal 作用及实际应用:
- 作用:ThreadLocal 为每个线程提供独立的变量副本,每个线程对该变量的操作都是独立的,互不干扰。这样可以避免多线程环境下的线程安全问题,特别是在一些需要每个线程有自己独立数据的场景。
- 实际应用:
- 数据库连接:在多线程 Web 应用中,每个线程需要独立的数据库连接。通过 ThreadLocal 可以为每个线程创建并管理自己的数据库连接,避免多个线程共享同一个连接导致的数据混乱。
- 用户会话管理:在 Web 应用中,每个用户的会话信息(如登录状态、用户信息等)可以通过 ThreadLocal 存储在当前线程中,方便在整个请求处理过程中随时获取和使用,且不会相互干扰。
- Spring 中 IOC 和 AOP:
- IOC(控制反转):
- 概念:将对象的创建和管理控制权从应用程序代码转移到 Spring 容器中。在传统编程中,对象的创建和依赖关系管理由应用程序自己负责,而在 Spring 中,通过配置(XML 或注解)告诉 Spring 容器需要创建哪些对象以及它们之间的依赖关系,Spring 容器负责创建和注入这些对象。
- 应用场景:在企业级应用开发中,对象之间的依赖关系复杂,使用 IOC 可以降低对象之间的耦合度,提高代码的可维护性和可测试性。例如,一个 Service 层的类可能依赖多个 DAO 层的类,通过 IOC,只需要在配置文件或使用注解声明依赖关系,Spring 容器会自动注入所需的 DAO 对象,Service 类无需关心具体的对象创建过程。
- AOP(面向切面编程):
- 概念:将一些与业务逻辑无关但又贯穿于多个业务模块的功能(如日志记录、事务管理、权限控制等)抽取出来,形成一个独立的切面,然后通过动态代理等技术将这些切面功能织入到目标业务逻辑中。
- 应用场景:
- 日志记录:在方法执行前后记录日志,记录方法的入参、出参以及执行时间等信息,方便调试和监控系统运行状态。
- 事务管理:在业务方法执行前开启事务,执行后提交或回滚事务,保证数据的一致性和完整性。通过 AOP 可以将事务管理逻辑从业务代码中分离出来,使业务代码更专注于业务逻辑实现。
- 权限控制:在方法调用前检查当前用户是否具有相应的权限,避免非法访问。
- Spring Boot 相对于 Spring 的优势及快速开发实现方式:
- 优势:
- 简化配置:Spring Boot 采用约定大于配置的原则,默认提供了很多合理的配置,大大减少了 Spring 项目中繁琐的 XML 配置或 Java 配置代码。例如,对于数据库连接,Spring Boot 只需要在配置文件中简单配置数据库相关参数,就可以自动配置好数据源、JdbcTemplate 等。
- 快速搭建项目:Spring Boot 提供了大量的 Starter 依赖,通过引入相应的 Starter,就可以快速集成各种功能,如 Spring Boot Starter Web 可以快速搭建一个 Web 应用,Spring Boot Starter DataSource 可以快速集成数据库相关功能。
- 生产级别的监控和管理:Spring Boot Actuator 提供了生产环境下的监控和管理功能,如查看应用的健康状态、查看运行时的各种指标(如内存使用情况、线程池状态等)。
- 快速开发实现方式:
- 自动配置:Spring Boot 根据项目中引入的依赖,自动配置相应的组件。例如,当引入 Spring Boot Starter JPA 依赖时,Spring Boot 会自动配置好 JPA 相关的 EntityManagerFactory、DataSource 等组件,开发者无需手动配置大量的 XML 或 Java 代码。
- Starter 依赖:通过引入各种 Starter 依赖,一站式引入项目所需的各种库和配置。例如,引入 Spring Boot Starter Thymeleaf 依赖,就可以快速集成 Thymeleaf 模板引擎,用于 Web 页面的渲染。
- MyBatis、Dubbo、RabbitMq、xxl - job、Redis 在项目中的作用:
- MyBatis:
- 作用:是一个优秀的持久层框架,用于数据库操作。它支持定制化 SQL、存储过程以及高级映射。MyBatis 可以将 Java 对象与 SQL 语句进行映射,通过 XML 或注解方式配置 SQL 语句,灵活地操作数据库。
- 应用场景:在企业级应用开发中,当需要对数据库进行复杂的 SQL 操作,如多表关联查询、动态 SQL 生成等场景下,MyBatis 非常适用。例如在电商系统中,查询商品信息可能涉及商品表、库存表、价格表等多表关联查询,MyBatis 可以方便地编写复杂的 SQL 语句来实现。
- Dubbo:
- 作用:是一个高性能的分布式服务框架,主要用于服务治理。它提供了服务注册与发现、负载均衡、远程调用等功能,使得不同的服务可以在分布式环境下进行高效的通信和协作。
- 应用场景:在大型分布式系统中,当存在多个服务需要相互调用时,Dubbo 可以帮助管理这些服务。例如,在一个电商系统中,订单服务可能需要调用库存服务、用户服务等,Dubbo 可以实现服务的注册与发现,让订单服务方便地找到并调用其他服务,同时通过负载均衡算法将请求均匀分配到多个服务实例上,提高系统的可用性和性能。
- RabbitMq:
- 作用:是一个消息队列中间件,主要用于实现异步处理、解耦系统组件以及流量削峰等功能。应用程序可以将消息发送到 RabbitMq 队列中,其他应用程序可以从队列中消费这些消息。
- 应用场景:
- 异步处理:例如在电商系统中,用户下单后,可能需要发送短信通知、更新积分等操作,这些操作可以通过消息队列异步处理,提高下单操作的响应速度,而不需要等待这些操作全部完成才返回结果给用户。
- 解耦系统组件:假设一个系统中有多个模块,如订单模块、库存模块、物流模块等,订单模块产生的消息可以发送到消息队列,库存模块和物流模块从队列中消费消息进行相应处理,这样各个模块之间的耦合度降低,模块的独立性和可维护性提高。
- 流量削峰:在高并发场景下,如电商的促销活动,大量的请求可能瞬间涌入系统。通过消息队列可以将这些请求先放入队列中,系统按照一定的速度从队列中取出请求进行处理,避免系统因瞬间高并发而崩溃。
- xxl - job:
- 作用:是一个轻量级分布式任务调度框架,提供了任务管理、调度、执行等功能。它支持 cron 表达式进行任务调度,并且可以在分布式环境下管理和执行任务。
- 应用场景:在企业级应用中,经常会有一些定时任务,如每天凌晨备份数据库、每月统计业务数据等。在分布式系统中,这些任务需要统一管理和调度。xxl - job 可以方便地配置和管理这些任务,并且可以根据服务器资源情况,将任务分配到不同的节点上执行,提高任务执行的效率和可靠性。
- Redis:
- 作用:是一个高性能的键值对存储数据库,通常用作缓存、消息队列、分布式锁等。它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,并且具有快速读写、持久化、集群等特性。
- 应用场景:
- 缓存:在 Web 应用中,将经常访问的数据(如热门商品信息、用户信息等)存储在 Redis 中,当用户请求这些数据时,先从 Redis 缓存中获取,减少数据库的访问压力,提高系统的响应速度。
- 分布式锁:在分布式系统中,多个节点可能同时竞争某些资源,通过 Redis 可以实现分布式锁,保证同一时间只有一个节点能够获取锁并执行相应操作,避免数据不一致问题。
- 消息队列:Redis 可以作为简单的消息队列使用,