在一间明亮却透着些许严肃氛围的会议室里,一场互联网大厂Java求职者面试正在紧张进行。
面试官:“第一轮提问开始。先说说Java中ArrayList和HashMap的底层数据结构分别是什么?”
王铁牛:“ArrayList底层是数组,HashMap底层是数组加链表,JDK1.8 后引入了红黑树。”
面试官:“回答不错。那ArrayList在扩容时具体是怎么操作的?”
王铁牛:“当ArrayList的元素个数达到容量阈值时,会进行扩容,新容量是原容量的1.5倍,然后把原数组的元素复制到新数组。”
面试官:“很好。HashMap在什么情况下会发生哈希冲突,怎么解决哈希冲突的?”
王铁牛:“当不同的键计算出相同的哈希值时就会发生哈希冲突。JDK1.8之前通过链表解决,之后在链表长度大于8且数组长度大于64时,链表会转化为红黑树来优化查找性能。”
面试官:“第二轮提问。讲讲多线程中线程池的核心参数有哪些,分别有什么作用?”
王铁牛:“有核心线程数、最大线程数、存活时间、时间单位、任务队列。核心线程数是线程池初始化时创建的线程数,最大线程数是线程池能容纳的最大线程数,存活时间是线程在没有任务时最多存活的时间,时间单位就是存活时间对应的单位,任务队列用来存放提交但未执行的任务。”
面试官:“不错。那线程池的工作流程是怎样的?”
王铁牛:“当有任务提交时,先看核心线程是否都在执行任务,如果有空闲核心线程就直接执行任务;如果核心线程都在忙,就把任务放入任务队列;如果任务队列满了,再看是否达到最大线程数,没达到就创建新线程执行任务,达到了就按照拒绝策略处理任务。”
面试官:“那常见的线程池拒绝策略有哪些?”
王铁牛:“有AbortPolicy,直接抛出异常;CallerRunsPolicy,让调用者线程来执行任务;DiscardPolicy,直接丢弃任务;DiscardOldestPolicy,丢弃队列中最老的任务。”
面试官:“第三轮提问。Spring框架中IOC和AOP分别是什么,有什么作用?”
王铁牛:“IOC是控制反转,把对象的创建和管理交给Spring容器,降低了对象之间的耦合度。AOP是面向切面编程,在不修改原有代码的基础上,对业务逻辑进行增强,比如日志记录、事务管理。”
面试官:“那Spring Boot相比于Spring有什么优势?”
王铁牛:“Spring Boot简化了Spring应用的搭建和开发过程,它有自动配置功能,能快速整合各种框架,内置了服务器,开发部署更方便。”
面试官:“MyBatis中#{}和${}有什么区别?”
王铁牛:“呃……#{}是预编译处理,能防止SQL注入,${}是字符串替换,在某些情况下可能会有SQL注入风险。”
面试官:“Dubbo是什么,在微服务架构中有什么作用?”
王铁牛:“Dubbo是一个高性能的Java RPC框架,在微服务架构中用于服务治理,能实现服务的注册与发现、负载均衡、远程调用等。”
面试官:“RabbitMQ的应用场景有哪些?”
王铁牛:“可以用于异步处理,比如订单下单后,一些后续操作可以通过消息队列异步处理;还有解耦,不同系统之间通过消息队列通信,降低耦合度;还有流量削峰,在高并发时缓存请求。”
面试官:“xxl - job是什么,有什么特点?”
王铁牛:“它是一个分布式任务调度平台,特点嘛……就是简单易用,支持集群部署,有可视化管理界面。”
面试官:“Redis有哪些数据类型,分别适用于什么场景?”
王铁牛:“有字符串,适用于缓存简单数据;哈希,适合存储对象;列表,可用于消息队列;集合,适合去重场景;有序集合,适用于排行榜之类的场景。”
面试官:“好的,今天的面试就到这里。你整体基础知识掌握得还可以,但对于一些更深入的原理理解还有待加强。回去等我们的通知吧,无论结果如何,我们都会在一周内给你回复。”
答案:
- ArrayList和HashMap的底层数据结构:
- ArrayList:底层是数组结构。数组可以快速地根据索引访问元素,所以ArrayList支持快速随机访问。例如,要获取ArrayList中第n个元素,直接通过数组下标访问即可,时间复杂度为O(1)。
- HashMap:JDK1.8之前底层是数组加链表结构,JDK1.8及之后是数组加链表加红黑树结构。数组的每个位置称为桶(bucket),通过哈希函数计算键的哈希值,再对数组长度取模得到桶的索引。当不同键的哈希值计算到相同桶时,就会形成链表(拉链法解决哈希冲突)。当链表长度大于8且数组长度大于64时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,查找、插入、删除操作平均时间复杂度为O(log n),相比于链表的O(n)有很大提升。
- ArrayList扩容操作:
- ArrayList有一个容量(capacity)和实际元素个数(size)。当size达到容量阈值(一般是容量 - 1)时,会触发扩容。新容量是原容量的1.5倍(原容量右移一位再加原容量)。例如原容量为10,新容量就是15。然后通过System.arraycopy方法将原数组的元素复制到新数组中。扩容操作涉及内存的重新分配和数据复制,所以频繁扩容会影响性能,在初始化ArrayList时,如果能预估数据量大小,最好指定初始容量,以减少扩容次数。
- HashMap哈希冲突及解决:
- 哈希冲突发生:由于哈希函数的局限性,不同的键可能计算出相同的哈希值,从而导致哈希冲突。例如,两个不同的字符串“abc”和“cba”,经过哈希函数计算后可能得到相同的哈希值。
- 解决方法:
- JDK1.8之前:采用链表法。当发生哈希冲突时,在对应桶的位置以链表形式存储多个键值对。查找时,先通过哈希值找到桶,再遍历链表找到对应的键值对,平均时间复杂度为O(n),n为链表长度。
- JDK1.8及之后:在链表长度大于8且数组长度大于64时,链表会转化为红黑树。红黑树的查找、插入、删除操作平均时间复杂度为O(log n),提高了在哈希冲突较多时的查找性能。当红黑树节点数小于6时,又会退化为链表。
- 线程池核心参数及作用:
- 核心线程数(corePoolSize):线程池初始化时创建的线程数,这些线程会一直存活,即使处于空闲状态,除非设置了allowCoreThreadTimeOut为true。例如,一个任务队列中有任务不断进来,线程池会优先使用核心线程来处理任务。
- 最大线程数(maximumPoolSize):线程池能容纳的最大线程数。当任务队列已满,且核心线程都在忙碌时,如果此时任务数超过核心线程数,线程池会创建新线程,直到线程数达到最大线程数。
- 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在没有任务执行的情况下,最多存活的时间。例如,线程池当前有10个线程,核心线程数为5,当有5个线程空闲时间超过存活时间,这些线程会被销毁。
- 时间单位(unit):存活时间对应的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- 任务队列(workQueue):用来存放提交但未执行的任务。常见的任务队列有ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列,不存储任务,直接提交给线程处理)等。不同的任务队列特性会影响线程池的性能和行为。
- 线程池工作流程:
- 当有任务提交到线程池时:
- 首先检查核心线程是否都在执行任务,如果有空闲的核心线程,就直接将任务分配给空闲核心线程执行。
- 如果核心线程都在忙碌,任务会被放入任务队列。
- 如果任务队列已满,再检查当前线程数是否达到最大线程数。若未达到,会创建新线程来执行任务;若已达到最大线程数,就按照拒绝策略处理任务。
- 当有任务提交到线程池时:
- 常见线程池拒绝策略:
- AbortPolicy:直接抛出RejectedExecutionException异常,阻止系统正常运行。例如,在一个对任务处理非常严格,不允许任务丢失的场景下,如果线程池无法处理新任务,采用此策略可以及时发现问题。
- CallerRunsPolicy:让调用者线程来执行任务。这样做的好处是不会丢弃任务,也不会抛出异常,但可能会影响调用者线程的性能。比如在一个Web应用中,当线程池满时,由处理请求的主线程来执行任务,可能会导致后续请求响应变慢。
- DiscardPolicy:直接丢弃任务,不做任何处理。适用于对任务可靠性要求不高,允许部分任务丢失的场景,如一些日志记录任务。
- DiscardOldestPolicy:丢弃队列中最老的任务(即将队列头部的任务丢弃),然后尝试提交新任务。这种策略适用于希望优先处理新任务的场景。
- Spring框架中IOC和AOP:
- IOC(控制反转):
- 概念:把对象的创建和管理交给Spring容器,而不是由应用程序自己创建和管理对象。例如,在传统Java开发中,一个类A依赖类B,需要在类A中通过new关键字创建类B的实例。而在Spring中,通过配置或注解,由Spring容器来创建和注入类B的实例到类A中。
- 作用:降低了对象之间的耦合度。使得代码的可维护性和可测试性增强。比如类A和类B的实现发生变化,只需要修改Spring配置或相关注解,而不需要在类A的代码中修改创建类B实例的逻辑。
- AOP(面向切面编程):
- 概念:在不修改原有业务逻辑代码的基础上,对业务逻辑进行增强。例如,在一个电商系统中,订单处理的业务逻辑是核心功能,而日志记录、事务管理等功能可以通过AOP切面来实现,不影响订单处理的核心代码。
- 作用:实现了业务逻辑的模块化和复用。将通用的功能(如日志、事务)抽取出来,以切面的形式应用到多个业务逻辑中,提高了代码的复用性和可维护性。
- IOC(控制反转):
- Spring Boot相比于Spring的优势:
- 简化搭建和开发:Spring Boot提供了大量的starter依赖,通过引入相关的starter,就能快速整合各种框架,如Spring Boot Starter for MyBatis,引入后就能方便地使用MyBatis进行数据库操作。
- 自动配置:Spring Boot能根据项目中引入的依赖自动进行配置。例如,引入了MySQL依赖和Spring Data JPA依赖,Spring Boot就能自动配置好数据源、JPA相关的配置,开发者无需手动编写大量的配置文件。
- 内置服务器:Spring Boot内置了Tomcat、Jetty等服务器,开发完成后可以直接打包成可执行的jar或war文件,直接运行,无需像Spring项目那样部署到外部服务器,开发部署更方便快捷。
- MyBatis中#{}和${}的区别:
- #{}:是预编译处理。MyBatis在处理#{}时,会将SQL中的#{}替换为?占位符,然后使用PreparedStatement进行设置参数值。例如,SQL语句为“SELECT * FROM user WHERE username = #{username}”,实际执行时会变为“SELECT * FROM user WHERE username =?”,然后通过PreparedStatement的setString方法设置参数值。这样能有效防止SQL注入攻击,因为参数值不会被解析为SQL语句的一部分。
- **{}时,会直接将{username}”,如果username变量值为“admin OR 1 = 1”,那么实际执行的SQL语句就是“SELECT * FROM user WHERE username = admin OR 1 = 1”,这就导致了SQL注入风险。所以在使用${}时,要确保传入的值是安全的,一般用于传入表名、列名等,且要对传入值进行严格校验。
- Dubbo在微服务架构中的作用:
- 服务注册与发现:Dubbo提供了服务注册中心(如Zookeeper、Nacos等),服务提供者将自己的服务注册到注册中心,服务消费者从注册中心获取服务提供者的地址信息。这样实现了服务的自动发现,消费者无需手动配置服务提供者的地址,提高了系统的可维护性和扩展性。例如,当服务提供者的地址发生变化时,注册中心会更新信息,消费者能及时获取到新地址。
- 负载均衡:当有多个服务提供者提供相同的服务时,Dubbo能实现负载均衡,将请求均匀地分配到各个服务提供者上。常见的负载均衡算法有随机、轮询、最少活跃调用数等。例如,随机算法会随机选择一个服务提供者来处理请求,轮询算法会按照顺序依次选择服务提供者,最少活跃调用数算法会优先选择当前活跃调用数最少的服务提供者,以提高系统的整体性能和可用性。
- 远程调用:Dubbo采用高性能的RPC(远程过程调用)协议,实现了不同服务之间的远程调用。服务消费者可以像调用本地方法一样调用远程服务提供者的方法,隐藏了远程调用的细节,使得微服务之间的通信更加简单和高效。
- RabbitMQ的应用场景:
- 异步处理:例如在电商系统中,用户下单后,除了处理订单核心逻辑,还可能需要发送短信通知、更新库存、记录日志等操作。这些操作可以通过消息队列异步处理,将相关任务发送到RabbitMQ队列中,订单处理主线程继续执行后续逻辑,提高了系统的响应速度。
- 解耦:不同系统之间通过RabbitMQ进行通信,降低了系统之间的耦合度。比如一个电商系统和一个物流系统,电商系统将订单发货信息发送到RabbitMQ队列,物流系统从队列中获取信息进行处理。如果电商系统或物流系统的实现发生变化,只要消息格式不变,就不会影响对方系统。
- 流量削峰:在高并发场景下,如秒杀活动,大量请求瞬间涌入。可以将请求先发送到RabbitMQ队列中,系统按照一定的速率从队列中获取请求进行处理,避免后端系统因瞬间高并发而崩溃。
- xxl - job特点:
- 简单易用:提供了简洁的API和可视化管理界面,开发者可以方便地进行任务的创建、调度、监控等操作。无需复杂的配置和开发,就能快速搭建起分布式任务调度系统。
- 支持集群部署:可以部署多个xxl - job执行器节点,提高任务处理的并发能力和可靠性。当某个节点出现故障时,其他节点可以继续处理任务,保证任务的正常执行。
- 可视化管理界面:通过可视化界面,管理员可以直观地查看任务的执行状态、执行日志、调度计划等信息。方便对任务进行管理和维护,及时发现和解决问题。
- Redis数据类型及适用场景:
- 字符串(String):
- 数据结构:简单的键值对,值可以是字符串、数字等。
- 适用场景:缓存简单数据,如用户信息、配置参数等。例如,将用户的基本信息(姓名、年龄等)以JSON字符串的形式存储在Redis中,下次获取用户信息时直接从Redis读取,减少数据库查询次数。也可以用于计数场景,如记录网站的访问量,通过INCR命令对存储访问量的字符串值进行递增操作。
- 哈希(Hash):
- 数据结构:类似Java中的Map,以字段 - 值的形式存储数据。
- 适用场景:适合存储对象。例如,存储用户的详细信息,每个字段对应一个属性,如“user:1”这个键下,字段“name”对应值“张三”,字段“age”对应值“25”。相比于字符串存储对象,哈希结构在获取部分属性时更高效,不需要获取整个对象字符串再解析。
- 列表(List):
- 数据结构:基于链表实现,可以从两端插入和删除元素。
- 适用场景:可用于消息队列。例如,生产者将消息从列表的一端插入,消费者从另一端取出消息进行处理。还可以用于记录最新的操作记录,如用户的登录记录,每次登录将记录插入到列表头部,通过限制列表长度可以只保留最新的若干条记录。
- 集合(Set):
- 数据结构:无序且唯一的键集合。
- 适用场景:适合去重场景。比如统计网站的独立访客,每次有新访客访问时,将访客ID加入到Redis的集合中,由于集合的唯一性,重复的访客ID不会被加入,通过获取集合的元素个数就能得到独立