第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 之后有红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:呃,好像是当元素个数达到容量的一定比例,就会扩容,新容量好像是原来的 1.5 倍,然后把旧数组的数据复制到新数组。 面试官:嗯,基本思路对。那 HashMap 在什么情况下会发生扩容? 王铁牛:当元素个数达到负载因子乘以容量的时候就扩容。
第二轮面试 面试官:接下来问问多线程相关。讲讲线程池的核心参数有哪些? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。 面试官:很好。那线程池处理任务的流程是怎样的? 王铁牛:呃,任务来了先看核心线程有没有满,没满就创建核心线程处理,满了就放队列里,队列满了就看最大线程数,没达到就创建非核心线程处理,要是都满了就触发拒绝策略。 面试官:还行。那在高并发场景下,使用线程池可能会遇到什么问题? 王铁牛:嗯……可能会出现线程资源耗尽吧,还有……还有可能任务处理不过来。
第三轮面试 面试官:聊聊框架相关,Spring 中 Bean 的生命周期是怎样的? 王铁牛:呃,先实例化,然后属性注入,接着初始化,最后销毁。 面试官:好。那 Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 自动配置,能快速搭建项目,减少配置。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:Dubbo 主要解决了什么问题? 王铁牛:嗯……好像是服务治理,服务之间的调用啥的。 面试官:RabbitMQ 在项目中有什么作用? 王铁牛:可以做消息队列,异步处理,削峰填谷。 面试官:xxl - job 是什么,有什么应用场景? 王铁牛:是个分布式任务调度平台,能做定时任务啥的。 面试官:Redis 有哪些数据类型,在缓存场景下怎么选择合适的数据类型? 王铁牛:有字符串、哈希、列表、集合、有序集合。缓存场景下,简单的键值对就用字符串,复杂点的对象用哈希。
面试总结:从整体面试情况来看,你对于一些基础的知识点掌握得还可以,像 ArrayList、HashMap 的底层结构,线程池的核心参数等回答得不错。但在一些稍微深入和复杂的问题上,比如高并发场景下线程池的问题,回答得不是特别清晰全面。对于框架方面,虽然能说出一些基本概念,但对于它们在实际业务场景中的深度应用和细节,还需要进一步加强理解。我们会综合评估所有面试者的情况,你回去等通知吧,无论结果如何,希望你在技术学习上能继续保持热情,不断提升自己。
问题答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它通过数组来存储元素,支持随机访问,因为数组的内存地址是连续的。例如,当我们创建一个
ArrayList<Integer> list = new ArrayList<>();时,在底层会先初始化一个默认容量(通常是 10)的数组来存储Integer类型的数据。 - HashMap:JDK1.8 之前底层是数组 + 链表结构,JDK1.8 之后是数组 + 链表 + 红黑树结构。数组的每个位置称为桶(bucket),通过哈希算法计算键的哈希值,然后对数组长度取模得到桶的索引位置。如果该位置没有元素,则直接放入;如果有元素,就形成链表(当链表长度大于 8 且数组容量大于 64 时,链表会转换为红黑树)。比如
HashMap<String, Integer> map = new HashMap<>();,当我们map.put("key", 10);时,会先计算 “key” 的哈希值,确定桶的位置进行存储。
- ArrayList:底层是数组结构,它通过数组来存储元素,支持随机访问,因为数组的内存地址是连续的。例如,当我们创建一个
- ArrayList 扩容操作:
- 当 ArrayList 中的元素个数达到其容量(capacity)的一定比例(默认是 0.9)时,会触发扩容。新容量是原来容量的 1.5 倍(
newCapacity = oldCapacity + (oldCapacity >> 1))。然后会创建一个新的更大的数组,通过System.arraycopy()方法将旧数组中的元素复制到新数组中。例如,初始容量为 10 的 ArrayList,当添加第 11 个元素时,就会扩容为 15。
- 当 ArrayList 中的元素个数达到其容量(capacity)的一定比例(默认是 0.9)时,会触发扩容。新容量是原来容量的 1.5 倍(
- HashMap 扩容情况:
- 当 HashMap 中的元素个数(size)达到负载因子(loadFactor,默认是 0.75)乘以容量(capacity)时,就会触发扩容。扩容时,新容量是原来容量的 2 倍。例如,初始容量为 16 的 HashMap,负载因子为 0.75,当元素个数达到
16 * 0.75 = 12时,就会扩容为 32。扩容后需要重新计算每个元素在新数组中的位置,因为哈希值对新容量取模的结果可能会改变。
- 当 HashMap 中的元素个数(size)达到负载因子(loadFactor,默认是 0.75)乘以容量(capacity)时,就会触发扩容。扩容时,新容量是原来容量的 2 倍。例如,初始容量为 16 的 HashMap,负载因子为 0.75,当元素个数达到
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了
allowCoreThreadTimeOut为true。 - 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数,当任务队列满了且核心线程都在忙碌时,会创建新的线程,直到达到最大线程数。
- 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,多余的线程会被销毁。
- 时间单位(unit):存活时间的单位,如
TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。 - 任务队列(workQueue):用于存放等待执行的任务,常见的任务队列有
ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认使用)、SynchronousQueue(不存储任务,直接交给线程处理)等。
- 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了
- 线程池处理任务流程:
- 当有新任务提交到线程池时,首先判断核心线程数是否已满。如果未满,创建新的核心线程来处理任务。
- 如果核心线程数已满,任务会被放入任务队列中。
- 如果任务队列也已满,再判断当前线程数是否达到最大线程数。若未达到,创建新的非核心线程来处理任务。
- 如果线程数已经达到最大线程数,此时会触发拒绝策略。常见的拒绝策略有
AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给提交任务的线程处理)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交当前任务)。
- 高并发场景下线程池可能遇到的问题:
- 线程资源耗尽:如果任务提交速度过快,超过了线程池的处理能力,且任务队列也已满,不断创建新线程直到达到最大线程数,可能会导致系统资源耗尽,如内存溢出等问题。
- 任务处理延迟:当任务队列过长,任务在队列中等待时间过长,会导致任务处理延迟,影响系统的响应时间。
- 线程上下文切换开销:过多的线程会导致频繁的线程上下文切换,消耗 CPU 资源,降低系统性能。
- Spring 中 Bean 的生命周期:
- 实例化(Instantiation):通过构造函数或者工厂方法创建 Bean 的实例对象。例如,对于一个简单的
@Component注解的类UserService,Spring 容器会在启动时为其创建实例。 - 属性注入(Populate):将 Bean 依赖的其他 Bean 通过 setter 方法、构造函数或者字段注入的方式进行赋值。比如
UserService依赖UserDao,可以通过@Autowired注解将UserDao注入到UserService中。 - 初始化(Initialization):调用 Bean 的初始化方法(可以通过
@PostConstruct注解标注的方法,或者实现InitializingBean接口的afterPropertiesSet方法,或者在配置文件中指定的init - method),进行一些初始化操作,如数据库连接的建立等。 - 销毁(Destruction):当 Bean 不再需要时,Spring 容器会调用其销毁方法(可以通过
@PreDestroy注解标注的方法,或者实现DisposableBean接口的destroy方法,或者在配置文件中指定的destroy - method),进行资源释放等操作,如关闭数据库连接等。
- 实例化(Instantiation):通过构造函数或者工厂方法创建 Bean 的实例对象。例如,对于一个简单的
- Spring Boot 相对于 Spring 的优势:
- 自动配置:Spring Boot 基于约定大于配置的原则,能根据项目的依赖自动配置 Spring 应用,大大减少了手动配置的工作量。例如,引入
spring - boot - starter - jdbc依赖,Spring Boot 就能自动配置好数据源、JdbcTemplate 等相关组件。 - 快速搭建项目:使用 Spring Boot 可以快速创建一个可运行的 Spring 项目,内置了 Tomcat、Jetty 等服务器,直接打包成可执行的 jar 或 war 文件,方便部署。
- 简化依赖管理:Spring Boot 的
parent项目定义了一系列常用依赖的版本号,开发者无需手动指定版本,减少了版本冲突的问题。
- 自动配置:Spring Boot 基于约定大于配置的原则,能根据项目的依赖自动配置 Spring 应用,大大减少了手动配置的工作量。例如,引入
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译方式,MyBatis 在处理
#{}时,会将 SQL 中的#{}替换为?,然后使用PreparedStatement进行设置参数值,这样可以有效防止 SQL 注入。例如,select * from user where username = #{username},在执行时会变成select * from user where username =?,然后通过PreparedStatement.setString(1, "具体用户名")来设置参数。 - **{}
时,会直接将{username}',如果username变量值为 “admin' or '1'='1”,就会导致 SQL 注入,因为最终的 SQL 会变成select * from user where username = 'admin' or '1'='1'`。
- #{}:是预编译方式,MyBatis 在处理
- Dubbo 主要解决的问题:
- 服务治理:Dubbo 是一个分布式服务框架,主要解决了微服务架构中服务之间的调用、注册、发现、负载均衡、容错等问题。它提供了服务注册中心(如 Zookeeper),服务提供者将自己的服务注册到注册中心,服务消费者从注册中心获取服务提供者的地址,然后进行远程调用。通过负载均衡算法(如随机、轮询等),可以将请求均匀分配到多个服务提供者上,提高系统的可用性和性能。同时,Dubbo 还支持多种容错机制,如失败重试、快速失败等,保证服务调用的可靠性。
- RabbitMQ 在项目中的作用:
- 异步处理:在一些业务场景中,比如用户注册成功后需要发送邮件通知、生成用户积分等操作,这些操作可以通过 RabbitMQ 异步处理,将任务发送到消息队列中,主线程继续执行其他业务逻辑,提高系统的响应速度。
- 削峰填谷:在高并发场景下,如电商的秒杀活动,大量的请求同时到达服务器,可能会导致服务器压力过大甚至崩溃。通过 RabbitMQ 作为消息队列,可以将请求先放入队列中,服务器按照自己的处理能力从队列中依次取出任务进行处理,避免瞬间高并发对系统造成冲击。
- xxl - job 是什么及应用场景:
- xxl - job:是一个轻量级分布式任务调度平台,它提供了简单易用的任务调度管理功能。
- 应用场景:
- 定时任务:比如每天凌晨执行数据备份、统计报表生成等定时任务。
- 分布式任务:在分布式系统中,有些任务需要在多个节点上同时执行,xxl - job 可以方便地管理这些分布式任务的调度和执行。
- Redis 数据类型及缓存场景选择:
- 字符串(String):最基本的数据类型,可以存储任何类型的数据,如字符串、数字等。在缓存简单的键值对数据时非常适用,比如缓存用户的基本信息(用户名、年龄等),可以将用户 ID 作为键,用户信息的 JSON 字符串作为值存储。
- 哈希(Hash):用于存储对象,它将对象的每个字段和值存储为一个键值对。适用于缓存复杂对象,例如缓存用户详细信息,每个字段(如姓名、性别、地址等)作为哈希的一个字段,值作为对应字段的值,这样可以方便地对对象的部分字段进行更新,而不需要重新缓存整个对象。
- 列表(List):是一个有序的字符串列表,可以从两端插入和删除元素。适用于实现消息队列(如简单的生产者 - 消费者模型),或者存储一些有序的数据,如最新的文章列表等。
- 集合(Set):无序的字符串集合,并且集合中的元素是唯一的。可以用于去重场景,比如统计网站的独立访客,将访客的 IP 地址存储在集合中,由于集合的唯一性,重复的 IP 不会被存储。
- 有序集合(Sorted Set):和集合类似,但每个元素都关联一个分数(score),根据分数进行排序。适用于排行榜场景,比如游戏玩家的积分排行榜,将玩家 ID 作为元素,积分作为分数存储在有序集合中,通过分数可以方便地获取排名信息。