第一轮面试 面试官:首先问几个基础问题。Java中ArrayList和HashMap的底层数据结构分别是什么? 王铁牛:ArrayList底层是数组,HashMap底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那ArrayList在扩容时具体是怎么操作的? 王铁牛:嗯……好像是当元素个数达到容量的一定比例,就会扩容,新容量大概是原来的1.5倍,然后把旧数组的元素复制到新数组。 面试官:回答得可以。那HashMap在什么情况下会发生扩容? 王铁牛:当HashMap里的元素个数达到负载因子(默认0.75)乘以当前容量的时候,就会扩容。
第二轮面试 面试官:接下来问些多线程相关的。讲讲线程池的核心参数有哪些? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。 面试官:很好。那线程池处理任务的流程是怎样的? 王铁牛:嗯……任务来了,先看核心线程有没有满,没满就创建核心线程处理,满了就放队列里,队列满了再看最大线程数,没达到就创建非核心线程处理,要是达到最大线程数了,就按照拒绝策略处理。 面试官:还行。那JUC包下的CountDownLatch是做什么的? 王铁牛:呃……好像是用来让一个线程等待其他线程完成一些操作后再继续执行。
第三轮面试 面试官:最后问些框架相关的。Spring的IOC和AOP分别是什么? 王铁牛:IOC是控制反转,把对象的创建和管理交给Spring容器。AOP是面向切面编程,在不修改原有代码的基础上,对方法进行增强。 面试官:回答得还行。那Spring Boot自动配置的原理是什么? 王铁牛:这个……好像是通过一些配置类和条件注解,Spring Boot能自动配置一些常用的组件。 面试官:那MyBatis的一级缓存和二级缓存有什么区别? 王铁牛:一级缓存是SqlSession级别的,在同一个SqlSession内有效。二级缓存是mapper级别的,多个SqlSession可以共享。 面试官:好的,今天的面试就到这里。你回去等通知吧,我们会综合评估所有候选人后,再决定是否录用你。感谢你今天来参加面试。
问题答案:
- ArrayList和HashMap的底层数据结构:
- ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。例如
list.get(0)可以直接获取到第一个元素。数组的特点是内存连续,所以随机访问效率高。 - HashMap:JDK1.7及之前,底层是数组加链表。数组的每个位置是一个链表的头节点,当发生哈希冲突时(不同的键计算出相同的哈希值),就以链表的形式存储在数组的同一个位置。JDK1.8 后,当链表长度大于8且数组容量大于64时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,查找、插入、删除的时间复杂度为O(log n),相比链表的O(n)在大数据量下效率更高。
- ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。例如
- ArrayList扩容操作:
- ArrayList有一个默认初始容量(通常是10),当向ArrayList中添加元素时,会检查当前元素个数是否达到了数组的容量。如果达到了,就会进行扩容。
- 扩容时,新的容量是原来容量的1.5倍(通过
oldCapacity + (oldCapacity >> 1)计算,oldCapacity >> 1表示将原容量右移一位,相当于除以2)。 - 然后会创建一个新的更大的数组,并将原数组中的元素复制到新数组中。这一过程涉及到内存的重新分配和数据复制,所以频繁扩容会影响性能。
- HashMap扩容条件:
- HashMap有一个负载因子(loadFactor),默认值是0.75。还有一个容量(capacity),初始容量通常是16。
- 当HashMap中的元素个数(size)达到
loadFactor * capacity时,就会触发扩容。例如,初始容量为16,负载因子为0.75,当元素个数达到16 * 0.75 = 12时,就会进行扩容。 - 扩容时,新的容量是原来容量的2倍,并且会重新计算每个元素在新数组中的位置,这是因为哈希值与数组长度有关,长度变化后哈希值对应的位置也会改变。
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池中会一直存活的线程数量,即使这些线程处于空闲状态,也不会被销毁(除非设置了allowCoreThreadTimeOut为true)。例如,一个Web服务器的线程池,核心线程数可以设置为处理日常请求量所需的线程数。
- 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数量。当任务队列已满,且核心线程数都在忙碌时,会创建新的线程,直到达到最大线程数。
- 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,这些空闲线程会被销毁。比如,在一个批处理任务的线程池中,任务执行完后,多余的线程在存活时间后会被回收。
- 时间单位(unit):存活时间的单位,如TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)等。当核心线程都在忙碌时,新的任务会被放入任务队列。
- 线程池处理任务流程:
- 当有新任务提交到线程池时,首先会判断核心线程数是否已满。如果核心线程数未满,就创建一个新的核心线程来处理该任务。
- 如果核心线程数已满,任务会被放入任务队列。
- 如果任务队列也已满,此时会判断线程数是否达到最大线程数。若未达到,就创建一个非核心线程来处理任务。
- 如果线程数已经达到最大线程数,那么就会按照设定的拒绝策略来处理该任务。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(让提交任务的线程自己执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
- CountDownLatch作用:
- CountDownLatch是JUC包下的一个同步工具类。它允许一个或多个线程等待其他一组线程完成操作后再继续执行。
- 例如,在一个多线程计算任务中,有多个子线程分别计算不同部分的数据,主线程需要等待所有子线程计算完成后,再汇总结果。这时就可以使用CountDownLatch。主线程调用
countDownLatch.await()方法进入等待状态,子线程在完成自己的任务后调用countDownLatch.countDown()方法,当所有子线程都调用了countDown(),使计数变为0时,主线程就会从await()方法处继续执行。
- Spring的IOC和AOP:
- IOC(控制反转):传统的Java开发中,对象的创建和管理由应用程序自己负责,这导致对象之间的耦合度较高。而Spring的IOC将对象的创建和管理交给Spring容器。例如,一个Service类依赖另一个Dao类,在IOC模式下,Spring容器会创建并注入Dao对象到Service类中,Service类不需要自己去创建Dao对象。这样使得代码的可测试性和可维护性大大提高,降低了对象之间的耦合度。
- AOP(面向切面编程):它是一种编程范式,旨在将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来。以日志记录为例,在传统编程中,可能需要在每个业务方法中手动添加日志记录代码,这会导致代码重复且难以维护。而使用AOP,可以定义一个切面,在切面中定义日志记录的逻辑,然后通过配置将这个切面应用到需要记录日志的方法上,这样就可以在不修改原有业务方法代码的基础上,为方法添加日志记录功能。
- Spring Boot自动配置原理:
- Spring Boot通过大量的自动配置类(以
AutoConfiguration结尾)来实现自动配置。这些自动配置类会根据项目中引入的依赖和配置属性来决定是否生效。 - 例如,当项目中引入了
spring - boot - starter - web依赖时,Spring Boot会自动配置Tomcat服务器、Spring MVC等相关组件。这是因为WebMvcAutoConfiguration类会根据类路径下是否存在DispatcherServlet等类,以及相关的配置属性(如server.port等)来进行自动配置。 - 自动配置还使用了条件注解(如
@ConditionalOnClass、@ConditionalOnProperty等)。@ConditionalOnClass表示当类路径下存在某个类时,该自动配置类才生效;@ConditionalOnProperty表示当配置文件中存在某个属性时,该自动配置类才生效。通过这些条件注解和自动配置类的组合,Spring Boot实现了灵活且智能的自动配置功能。
- Spring Boot通过大量的自动配置类(以
- MyBatis一级缓存和二级缓存区别:
- 一级缓存:是SqlSession级别的缓存。在同一个SqlSession内,执行相同的SQL查询时,MyBatis会先从一级缓存中查找结果,如果找到了就直接返回,不会再去数据库查询。例如,在一个Service方法中,多次调用同一个Mapper方法查询相同数据,只要是在同一个SqlSession内,就会使用一级缓存。一级缓存的生命周期与SqlSession一致,当SqlSession关闭或提交事务时,一级缓存会被清空。
- 二级缓存:是mapper级别的缓存,多个SqlSession可以共享。当开启二级缓存后,一个SqlSession查询的数据会被缓存起来,其他SqlSession也可以使用这个缓存的数据。二级缓存的生命周期比一级缓存长,它会在应用程序运行期间一直存在(除非手动清除)。不过,使用二级缓存时要注意数据的一致性问题,因为多个SqlSession可能会对数据进行修改,所以在更新数据时,需要及时清理二级缓存。