《互联网大厂 Java 面试:从基础到进阶的核心知识大考察》

40 阅读13分钟

第一轮面试 面试官:先问你几个基础问题。讲讲Java中ArrayList和HashMap的底层原理。 王铁牛:ArrayList底层是数组,能动态扩容。HashMap底层是数组加链表,JDK 1.8后引入红黑树,当链表长度超过8且数组长度大于64时,链表转红黑树。 面试官:不错,回答得挺清楚。那HashMap在多线程环境下会有什么问题? 王铁牛:嗯……好像会有数据丢失啥的,具体不太清楚。 面试官:行吧。再说说Spring框架中IOC和AOP的概念。 王铁牛:IOC是控制反转,把对象创建和管理交给Spring容器。AOP是面向切面编程,能在不修改原有代码基础上增加功能,像日志记录、事务管理。 面试官:这回答得不错。

第二轮面试 面试官:接下来深入点问。Spring Boot自动配置原理是什么? 王铁牛:呃,就是Spring Boot能自动配置一些东西,根据类路径下的依赖啥的。 面试官:能说得更详细些吗?那MyBatis的一级缓存和二级缓存了解吗? 王铁牛:一级缓存是SqlSession级别的,同一个SqlSession内查询相同SQL会从缓存取数据。二级缓存是Mapper级别的,多个SqlSession可以共享。 面试官:还行。Dubbo的服务注册与发现机制是怎样的? 王铁牛:就是……好像是用Zookeeper啥的做注册中心,服务提供者把服务注册上去,消费者去获取服务。 面试官:嗯,回答得不算全面。

第三轮面试 面试官:最后一轮,问几个更难的。JUC包下的线程池ThreadPoolExecutor的核心参数有哪些,分别有什么作用? 王铁牛:有核心线程数、最大线程数……还有个队列啥的,具体作用不太记得清了。 面试官:那JVM的垃圾回收机制讲讲,CMS垃圾回收器的特点和流程。 王铁牛:CMS是并发收集器,能减少停顿时间。流程嘛,好像是初始标记、并发标记、重新标记、并发清除。 面试官:回答得不太清晰。RabbitMQ的消息确认机制和死信队列了解吗? 王铁牛:消息确认机制好像是确认消息有没有被接收,死信队列就是消息变成死信后进入的队列。 面试官:好,今天的面试就到这里。你回去等通知吧,我们会综合评估所有候选人后,再决定是否录用你。感谢你今天来参加面试。

问题答案

  1. ArrayList和HashMap的底层原理
    • ArrayList:底层基于数组实现。它有一个Object类型的数组elementData来存储元素。当向ArrayList中添加元素时,如果当前数组容量不足,会进行扩容。扩容机制是先计算新的容量(一般是原容量的1.5倍),然后创建一个新的更大的数组,将原数组的元素复制到新数组中。这样就实现了动态扩容,以适应不断增加的元素存储需求。
    • HashMap:JDK 1.8之前,底层由数组加链表组成。数组的每个元素是一个链表的头节点。当计算出的哈希值相同时(哈希冲突),会将元素以链表的形式存储在同一个数组位置。JDK 1.8后,当链表长度超过8且数组长度大于64时,链表会转换为红黑树,以提高查找效率。因为红黑树的查找时间复杂度为O(logn),相比链表的O(n)在数据量较大时性能更好。
  2. HashMap在多线程环境下的问题
    • 数据丢失:在多线程环境下,当多个线程同时进行put操作时,如果发生扩容,可能会导致数据丢失。因为扩容涉及到重新计算哈希值和重新分配元素到新的数组位置,多线程操作可能会使元素的迁移过程出现混乱。
    • 死循环:在JDK 1.7及之前的版本中,多线程下HashMap扩容时可能会形成环形链表,导致死循环。这是因为在扩容时,多线程同时操作链表的迁移,可能会使链表的指针指向出现错误,形成环形结构。当进行get操作遍历链表时,就会陷入死循环。
  3. Spring框架中IOC和AOP的概念
    • IOC(控制反转):传统编程中,对象的创建和依赖关系的管理由应用程序自身负责。而在Spring框架中,IOC将这种控制权反转给了Spring容器。Spring容器负责创建对象、管理对象的生命周期以及对象之间的依赖关系。例如,一个Service类可能依赖于一个Dao类,在IOC模式下,Spring容器会创建这两个类的实例,并将Dao实例注入到Service实例中,应用程序只需要使用这些对象,而不需要关心它们的创建和依赖管理过程。
    • AOP(面向切面编程):AOP是一种编程范式,它将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,形成独立的切面。这些切面可以在不修改原有业务代码的基础上,在特定的连接点(如方法调用前后、异常抛出时等)织入到业务逻辑中。例如,在一个业务方法执行前记录日志,在方法执行后进行事务提交,通过AOP可以很方便地实现这些功能,提高代码的可维护性和复用性。
  4. Spring Boot自动配置原理
    • Spring Boot的自动配置是基于条件配置的。它通过@EnableAutoConfiguration注解开启自动配置功能。在启动过程中,Spring Boot会扫描META - INF/spring.factories文件,该文件中定义了各种自动配置类。这些自动配置类会根据类路径下是否存在特定的依赖、配置属性等条件来决定是否生效。例如,如果类路径下存在Tomcat相关的依赖,Spring Boot会自动配置Tomcat作为Web服务器;如果存在MyBatis相关的依赖,会自动配置MyBatis相关的组件。同时,Spring Boot还提供了外部配置属性的机制,用户可以通过application.properties或application.yml文件来修改自动配置的默认值,实现个性化配置。
  5. MyBatis的一级缓存和二级缓存
    • 一级缓存:是SqlSession级别的缓存。当在同一个SqlSession内执行相同的SQL查询时,MyBatis会先从一级缓存中查找结果,如果存在则直接返回,不再执行数据库查询。一级缓存的作用域是SqlSession,当SqlSession关闭或提交事务时,一级缓存会被清空。例如,在一个Service方法中,多次调用同一个Mapper方法查询相同数据,只要SqlSession没有关闭或提交事务,第二次及以后的查询就会从一级缓存中获取数据,提高查询效率。
    • 二级缓存:是Mapper级别的缓存,多个SqlSession可以共享。二级缓存需要手动开启,在Mapper.xml文件中配置标签。当一个SqlSession查询数据后,会将数据放入二级缓存中,其他SqlSession查询相同数据时,如果二级缓存中有数据,则直接从二级缓存中获取。二级缓存的生命周期比一级缓存长,它在应用程序运行期间一直存在,除非手动清除或应用程序重启。但使用二级缓存时要注意数据一致性问题,因为多个SqlSession共享缓存,一个SqlSession对数据的修改可能不会及时反映到缓存中,需要合理设置缓存刷新策略。
  6. Dubbo的服务注册与发现机制
    • 服务注册:Dubbo的服务提供者在启动时,会将自己提供的服务信息(如服务接口、服务实现类、服务方法等)注册到注册中心。常用的注册中心有Zookeeper、Redis等,以Zookeeper为例,服务提供者会在Zookeeper的指定节点下创建临时节点,将服务信息存储在该节点中。这样,注册中心就知道了有哪些服务可供使用。
    • 服务发现:Dubbo的服务消费者在启动时,会从注册中心订阅自己需要的服务。注册中心会将服务提供者的地址列表返回给服务消费者。服务消费者会缓存这些地址列表,并使用负载均衡算法(如随机、轮询等)从地址列表中选择一个服务提供者进行调用。当服务提供者的地址发生变化(如新增、下线等)时,注册中心会通知服务消费者,服务消费者会更新自己的缓存地址列表,保证调用的正确性。
  7. JUC包下的线程池ThreadPoolExecutor的核心参数
    • corePoolSize(核心线程数):线程池中会一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut为true。当有新任务提交时,如果线程池中的线程数小于corePoolSize,会创建新的线程来处理任务。
    • maximumPoolSize(最大线程数):线程池中允许存在的最大线程数。当任务队列已满,且线程池中的线程数小于maximumPoolSize时,会创建新的线程来处理任务。但要注意,当线程数达到maximumPoolSize且任务队列也已满时,新提交的任务会根据拒绝策略进行处理。
    • keepAliveTime(线程存活时间):当线程池中的线程数大于corePoolSize时,多余的空闲线程在存活时间达到keepAliveTime后会被销毁。例如,如果设置keepAliveTime为10秒,那么当一个线程空闲时间超过10秒且线程池中的线程数大于corePoolSize时,该线程会被销毁。
    • unit(存活时间单位):keepAliveTime的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。
    • workQueue(任务队列):用于存放等待执行的任务。常用的任务队列有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)、SynchronousQueue(同步队列)等。当线程池中的线程都在忙碌时,新提交的任务会被放入任务队列中等待执行。不同的任务队列特性会影响线程池的性能和行为,例如无界队列可能会导致内存耗尽,而有界队列在队列满时会触发线程池创建新线程或执行拒绝策略。
    • threadFactory(线程工厂):用于创建线程的工厂。通过自定义线程工厂,可以设置线程的名称、优先级、是否为守护线程等属性。例如,可以创建一个线程工厂,为每个线程设置一个有意义的名称,方便在调试和监控时识别线程。
    • handler(拒绝策略):当任务队列已满且线程池中的线程数达到maximumPoolSize时,新提交的任务会被拒绝,此时会执行拒绝策略。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给调用者线程执行)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
  8. JVM的垃圾回收机制 - CMS垃圾回收器的特点和流程
    • 特点
      • 并发收集:CMS垃圾回收器在进行垃圾回收时,能让应用程序线程和垃圾回收线程同时运行,尽量减少应用程序的停顿时间。这对于对响应时间要求较高的应用程序非常重要。
      • 低停顿:通过并发收集,CMS能有效降低垃圾回收过程中应用程序的停顿时间,提高应用程序的响应速度。但它是以牺牲一定的吞吐量为代价的,因为垃圾回收线程会占用一部分CPU资源。
      • 标记 - 清除算法:CMS采用标记 - 清除算法,先标记出所有需要回收的对象,然后直接清除这些对象,不会进行对象的移动和整理。这就导致了在垃圾回收后可能会产生大量的内存碎片。
    • 流程
      • 初始标记:暂停所有应用程序线程,标记出与GC Roots直接相连的对象。这个阶段速度很快,因为只需要标记直接相连的对象,停顿时间较短。
      • 并发标记:应用程序线程和垃圾回收线程同时运行,从初始标记的对象开始,标记出所有可达对象。这个阶段耗时较长,但应用程序可以继续运行,不会产生长时间停顿。
      • 重新标记:再次暂停应用程序线程,修正并发标记期间因应用程序线程运行而导致的标记变动。这个阶段停顿时间比初始标记稍长,但比并发标记短很多。
      • 并发清除:应用程序线程和垃圾回收线程同时运行,回收标记为不可达的对象所占用的内存空间。在这个阶段,应用程序可以继续处理业务请求。
  9. RabbitMQ的消息确认机制和死信队列
    • 消息确认机制
      • 生产者确认:RabbitMQ提供了两种方式来确认消息是否成功发送到Broker。一种是通过事务机制,生产者发送消息前开启事务,发送消息后提交事务,如果提交事务失败则回滚。但事务机制会严重影响性能,因为它是同步阻塞的。另一种是使用confirm机制,生产者将信道设置为confirm模式,当消息成功到达Broker后,Broker会发送一个确认消息给生产者。生产者可以通过监听确认消息来判断消息是否发送成功。如果消息发送失败,生产者可以进行重试等操作。
      • 消费者确认:消费者从队列中获取消息后,需要向Broker发送确认消息,表明已经成功处理了该消息。RabbitMQ支持两种确认模式,自动确认和手动确认。自动确认模式下,消费者一旦接收到消息,RabbitMQ就认为消息已被成功处理,会立即从队列中删除该消息。手动确认模式下,消费者需要调用basicAck或basicNack方法来显式地告诉RabbitMQ消息是否处理成功。如果消费者没有发送确认消息,RabbitMQ会认为消息没有被成功处理,不会从队列中删除该消息,可能会重新将消息发送给其他消费者。
    • 死信队列:当一条消息在队列中出现以下情况时,会变成死信:
      • 消息被拒绝:消费者调用basicNack或basicReject方法拒绝消息,并且设置requeue参数为false,该消息就会进入死信队列。
      • 消息过期:可以为队列或消息设置过期时间(TTL,Time - To - Live),当消息在队列中存活时间超过设置的TTL时,会变成死信进入死信队列。
      • 队列达到最大长度:当队列中的消息数量达到设置的最大长度时,新进入队列的消息会被丢弃或进入死信队列,具体取决于配置。死信队列可以用于处理异常消息、重试失败的消息等场景。例如,可以将死信队列中的消息重新发送到原队列进行重试,或者进行特殊处理,如记录日志、通知管理员等。