在一间明亮却略显严肃的面试房间里,一位求职者正襟危坐在桌前,对面的面试官表情沉稳,一场决定命运的互联网大厂Java面试即将拉开帷幕。
面试官:“第一轮面试开始。先问几个基础问题,Java中ArrayList和HashMap的底层数据结构分别是什么?” 王铁牛:“ArrayList底层是数组,HashMap底层是数组加链表,JDK1.8后引入了红黑树。” 面试官:“回答得不错。那HashMap在什么情况下会发生扩容?” 王铁牛:“当HashMap中的元素个数达到负载因子(默认0.75)乘以当前容量时,就会进行扩容。” 面试官:“很好。接着问,多线程中线程的生命周期有哪几种状态?” 王铁牛:“新建、就绪、运行、阻塞、死亡这几种状态。” 面试官:“不错,基础掌握得还行。接下来第二轮。Spring框架中IOC和AOP分别是什么?” 王铁牛:“IOC是控制反转,就是把对象创建和对象之间的依赖关系交给Spring容器管理。AOP是面向切面编程,能把一些通用功能抽取出来,比如日志、事务,不影响主业务逻辑。” 面试官:“那Spring Boot自动配置的原理是什么?” 王铁牛:“嗯……就是它能根据类路径下的依赖自动配置一些Bean,好像是通过一些配置类和条件注解啥的。” 面试官:“不太清晰,再说说MyBatis中#{}和{}的区别。” **王铁牛**:“#{}是预编译处理,{}是字符串替换,#{}能防止SQL注入。” 面试官:“好,最后一轮。Dubbo的服务暴露过程是怎样的?” 王铁牛:“呃……就是通过一些协议把服务暴露出去,像什么dubbo协议,然后注册到注册中心。” 面试官:“具体点呢?那RabbitMQ的消息确认机制是怎样的?” 王铁牛:“有生产者确认,还有消费者确认,生产者确认就是消息发送到RabbitMQ后返回确认信息,消费者确认就是消费者处理完消息后给RabbitMQ返回确认。” 面试官:“那xxl - job的调度原理是什么?” 王铁牛:“它好像是有个调度中心,然后去调度任务,具体咋调度我不太清楚了。” 面试官:“好,面试到这里。整体来看,你基础知识部分掌握得还可以,但在一些进阶知识点上理解不够深入。我们会综合评估所有面试者,你回家等通知吧。”
问题答案:
- ArrayList和HashMap的底层数据结构:
- ArrayList:底层是数组结构,它允许动态扩容。当添加元素超出当前数组容量时,会创建一个新的更大的数组,并将原数组内容复制到新数组。这使得ArrayList在随机访问元素时效率很高,因为可以通过数组下标直接定位元素,但在插入和删除元素(非尾部)时,需要移动大量元素,效率较低。
- HashMap:JDK1.8之前,底层是数组加链表结构。数组的每个位置是一个链表的头节点,当发生哈希冲突时(不同的键计算出相同的哈希值),会将新的键值对以链表的形式挂在对应数组位置的链表上。JDK1.8后,当链表长度大于8且数组容量大于64时,链表会转换为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,能保证在最坏情况下,查找、插入和删除操作的时间复杂度为O(log n)。
- HashMap扩容机制:
- HashMap有两个重要参数:容量(capacity)和负载因子(load factor)。默认初始容量是16,负载因子默认是0.75。当HashMap中的元素个数(size)达到负载因子乘以当前容量(即size >= capacity * load factor)时,就会触发扩容。扩容时,新的容量是原来容量的2倍。然后会重新计算每个键值对在新数组中的位置,并将原数组中的元素重新分配到新数组中。这是因为扩容后数组大小改变,哈希值对应的数组位置可能会改变。
- 线程的生命周期状态:
- 新建(New):当创建一个Thread对象,但还未调用start()方法时,线程处于新建状态。此时线程还没有开始运行,只是一个对象。
- 就绪(Runnable):调用start()方法后,线程进入就绪状态。在这个状态下,线程已经具备了运行的条件,但还没有分配到CPU资源,等待CPU调度。
- 运行(Running):当线程获得CPU资源开始执行run()方法中的代码时,线程处于运行状态。
- 阻塞(Blocked):线程在运行过程中,可能会因为某些原因进入阻塞状态,比如调用了sleep()方法、wait()方法,或者在等待锁等。处于阻塞状态的线程不会被CPU调度,直到阻塞条件解除,重新回到就绪状态。
- 死亡(Dead):当线程的run()方法执行完毕,或者因为异常等原因提前终止,线程就进入死亡状态。此时线程的生命周期结束,不能再重新启动。
- Spring框架中IOC和AOP:
- IOC(控制反转):传统的Java开发中,对象的创建和依赖关系的管理由开发者自己负责,这使得代码的耦合度较高。IOC则是把对象的创建和依赖关系的管理交给Spring容器。例如,有一个Service类依赖于Dao类,在IOC模式下,Spring容器会创建Dao对象,并将其注入到Service对象中,Service类不需要自己去创建Dao对象。IOC通过依赖注入(Dependency Injection,DI)来实现,常见的依赖注入方式有构造函数注入、Setter方法注入和字段注入。
- AOP(面向切面编程):在软件开发中,有些功能是横切多个业务模块的,比如日志记录、事务管理、权限控制等。AOP就是把这些通用功能抽取出来,形成一个个切面(Aspect)。在Spring中,通过配置或注解的方式,将切面应用到目标方法或类上。例如,通过AOP可以在方法执行前记录日志,方法执行后进行事务提交等操作,而不影响主业务逻辑的代码结构,提高了代码的复用性和可维护性。
- Spring Boot自动配置原理:
- Spring Boot的自动配置是基于条件配置(Conditional Configuration)实现的。Spring Boot在启动时,会扫描classpath下的所有依赖。根据这些依赖,Spring Boot会通过一系列的条件注解(如@ConditionalOnClass、@ConditionalOnProperty等)来判断是否需要自动配置某个组件。例如,如果classpath下存在Tomcat相关的类,并且配置文件中没有禁用Tomcat,Spring Boot就会自动配置Tomcat作为Web服务器。Spring Boot还提供了大量的自动配置类(以AutoConfiguration结尾),这些类会根据条件注解来创建相应的Bean并注册到Spring容器中。同时,Spring Boot的starter依赖也起到了重要作用,每个starter依赖包含了相关功能的必要依赖和自动配置类,使得引入依赖变得更加简单和方便。
- MyBatis中#{}和${}的区别:
- #{}:#{}是预编译处理,MyBatis在处理#{}时,会将SQL中的#{}替换为?占位符,然后使用PreparedStatement进行设置参数值。这样可以有效防止SQL注入攻击,因为参数值会被当作字符串处理,不会与SQL语句混淆。例如,SQL语句为“SELECT * FROM user WHERE username = #{username}”,实际执行时会变为“SELECT * FROM user WHERE username =?”,然后通过PreparedStatement的setString方法设置参数值。
- **{}是字符串替换,MyBatis在处理{}中的内容替换到SQL语句中。如果参数值是字符串,不会添加单引号,这就容易导致SQL注入风险。例如,SQL语句为“SELECT * FROM user WHERE username = {}时,一定要确保参数值是安全的,通常用于传入表名、列名等不会引起SQL注入的场景。
- Dubbo的服务暴露过程:
- 配置解析:Dubbo服务提供者在启动时,会读取配置文件(如XML配置或注解配置),解析出服务接口、实现类、协议、端口、注册中心地址等信息。
- Proxy生成:Dubbo通过动态代理技术(如Javassist或JDK动态代理)为服务实现类生成代理对象。这个代理对象负责将调用请求转换为Dubbo协议的消息,并发送给消费者。
- 协议绑定:根据配置的协议(如dubbo协议、http协议等),Dubbo会创建相应的协议处理器。以dubbo协议为例,它会创建一个Netty服务器(默认使用Netty),绑定到指定的端口,监听消费者的请求。
- 注册中心注册:服务提供者将服务的元数据(包括服务接口、版本、分组、地址等信息)注册到注册中心(如Zookeeper、Nacos等)。注册中心会维护服务提供者的列表,并提供服务发现功能,让消费者可以获取到服务提供者的地址。
- RabbitMQ的消息确认机制:
- 生产者确认(Publisher Confirm):
- 开启确认模式:生产者通过调用channel.confirmSelect()方法开启确认模式。
- 消息发送与确认:当生产者发送消息后,RabbitMQ会给生产者发送一个确认消息(Basic.Ack),表示消息已经成功接收。如果消息发送失败(例如网络问题、队列不存在等),RabbitMQ会发送一个否定确认消息(Basic.Nack)。生产者可以通过添加ConfirmListener来处理这些确认消息,在监听器中可以根据确认结果进行相应的处理,比如重新发送消息。
- 消费者确认(Consumer Ack):
- 自动确认:默认情况下,RabbitMQ采用自动确认模式,即当消费者接收到消息后,RabbitMQ就认为该消息已被成功处理,会立即从队列中删除。这种模式简单,但可能会导致消息丢失,比如消费者在处理消息过程中崩溃,消息就丢失了。
- 手动确认:消费者可以通过设置autoAck=false来开启手动确认模式。在手动确认模式下,消费者处理完消息后,需要调用channel.basicAck(deliveryTag, multiple)方法向RabbitMQ发送确认消息。其中deliveryTag是消息的唯一标识,multiple表示是否批量确认。如果消费者处理消息失败,可以调用channel.basicNack(deliveryTag, multiple, requeue)方法,requeue表示是否将消息重新放回队列。
- 生产者确认(Publisher Confirm):
- xxl - job的调度原理:
- 调度中心:xxl - job有一个调度中心,它是整个调度系统的核心。调度中心负责管理任务,包括任务的新增、修改、删除,以及任务的调度配置(如调度周期、调度时间等)。调度中心基于数据库存储任务信息和调度日志等数据。
- 任务注册:执行器(JobExecutor)启动时,会向调度中心注册自己,并汇报自己能执行的任务列表。调度中心会维护一个执行器列表和任务与执行器的映射关系。
- 调度触发:调度中心根据任务的调度配置,定时触发任务调度。当任务触发时,调度中心会根据任务与执行器的映射关系,选择一个合适的执行器来执行任务。
- 任务执行:调度中心通过网络通信(如HTTP)将任务发送给选中的执行器。执行器接收到任务后,会开启一个线程池来执行具体的任务逻辑。任务执行完成后,执行器会将任务执行结果返回给调度中心,调度中心记录任务执行日志。