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

37 阅读11分钟

在一间明亮却略显严肃的面试室里,一场决定求职者命运的Java面试即将拉开帷幕。

面试官:好,我们开始面试。第一轮,先从Java基础问起。第一个问题,ArrayList和HashMap在存储结构上有什么区别?

王铁牛:ArrayList是基于数组的,能顺序存储数据。HashMap是基于哈希表,通过键值对存储。

面试官:回答得不错。第二个问题,HashMap在多线程环境下会出现什么问题?

王铁牛:嗯……好像会数据错乱啥的,具体不太清楚。

面试官:行吧,最后一个问题,Java中多线程创建有几种方式?

王铁牛:有继承Thread类,还有实现Runnable接口。

面试官:第一轮表现还行。接下来第二轮,我们聊聊JUC和多线程。第一个问题,线程池的核心参数有哪些,分别有什么作用?

王铁牛:有核心线程数,最大线程数……嗯,其他的不太记得了。

面试官:那说说线程池处理任务的流程是怎样的?

王铁牛:就是先看核心线程,然后……哎呀,有点乱。

面试官:最后一个问题,CountDownLatch和CyclicBarrier有什么区别?

王铁牛:好像一个只能用一次,一个能用多次,具体咋回事不太明白。

面试官:好,进入第三轮,谈谈框架相关。Spring的IOC和AOP是什么?

王铁牛:IOC是控制反转,AOP是面向切面编程。

面试官:不错。那Spring Boot自动配置原理是什么?

王铁牛:就是……好像是根据一些条件自动配置,具体咋实现不清楚。

面试官:MyBatis的一级缓存和二级缓存有什么区别?

王铁牛:一级缓存好像是在一个SqlSession内有效,二级缓存不太清楚。

面试官:Dubbo的服务暴露和引用流程是怎样的?

王铁牛:嗯……不太了解这个。

面试官:好,面试差不多了。从你的回答来看,基础部分掌握得还行,但在一些进阶和框架深入理解方面还有所欠缺。我们后续会综合评估所有面试者情况,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。

答案:

  1. ArrayList和HashMap在存储结构上的区别
    • ArrayList:基于动态数组实现,它按照顺序存储元素,元素在内存中是连续存储的。这种结构适合按顺序访问元素,查询效率较高(通过索引直接访问),但在插入和删除元素时,如果不是在末尾操作,需要移动大量元素,效率较低。
    • HashMap:基于哈希表实现,它通过键的哈希值来确定存储位置,以键值对的形式存储数据。哈希表的结构使得在理想情况下,插入、删除和查找操作的时间复杂度都接近O(1)。但如果哈希冲突严重,性能会下降,此时会通过链表或红黑树(JDK 1.8及以后,当链表长度达到一定阈值时转换为红黑树)来解决冲突。
  2. HashMap在多线程环境下会出现什么问题
    • 数据错乱:在多线程同时进行put操作时,可能会导致哈希表的结构被破坏,出现数据覆盖或丢失等情况。例如,在JDK 1.7及以前的版本中,当多个线程同时进行扩容操作时,可能会形成环形链表,导致在get操作时出现死循环。
    • 线程安全问题:HashMap本身不是线程安全的,多个线程同时读写HashMap可能会导致数据不一致。比如一个线程在读取数据,另一个线程在修改数据,可能会读到脏数据。
  3. Java中多线程创建有几种方式
    • 继承Thread类:创建一个类继承Thread类,重写run方法,在run方法中编写线程执行的逻辑。然后创建该类的实例,调用start方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行中");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
  • 实现Runnable接口:创建一个类实现Runnable接口,实现run方法。然后创建该类的实例,将其作为参数传递给Thread类的构造函数,再调用start方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行中");
    }
}
public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
  • 实现Callable接口:与Runnable类似,但Callable接口的call方法可以有返回值,并且可以抛出异常。需要通过FutureTask类来获取Callable任务的执行结果。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 100;
    }
}
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        Integer result = futureTask.get();
        System.out.println("结果:" + result);
    }
}
  1. 线程池的核心参数有哪些,分别有什么作用
    • 核心线程数(corePoolSize):线程池中常驻的核心线程数。即使这些线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut为true。当有新任务提交时,如果当前线程数小于核心线程数,会创建新的核心线程来处理任务。
    • 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。当任务队列已满,且当前线程数小于最大线程数时,会创建新的非核心线程来处理任务。
    • 任务队列(workQueue):用于存放等待执行的任务。当核心线程都在忙碌时,新提交的任务会被放入任务队列。常见的任务队列有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)、SynchronousQueue(同步队列)等。
    • 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程(非核心线程)在等待新任务到来的时间超过这个时间后,会被销毁。
    • 时间单位(unit):线程存活时间的单位,如TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 拒绝策略(RejectedExecutionHandler):当任务队列已满且线程数达到最大线程数时,新提交的任务会被拒绝,此时会执行拒绝策略。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给调用者线程执行)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
  2. 线程池处理任务的流程
    • 当有新任务提交到线程池时,首先判断当前线程数是否小于核心线程数。如果小于,会创建新的核心线程来处理任务。
    • 如果当前线程数已经达到核心线程数,任务会被放入任务队列。
    • 如果任务队列已满,再判断当前线程数是否小于最大线程数。如果小于,会创建新的非核心线程来处理任务。
    • 如果当前线程数已经达到最大线程数,且任务队列也已满,此时会执行拒绝策略,拒绝新提交的任务。
  3. CountDownLatch和CyclicBarrier有什么区别
    • CountDownLatch:它允许一个或多个线程等待,直到其他一组线程完成操作。它通过一个计数器来实现,当调用countDown方法时,计数器减1,当计数器减到0时,等待的线程被唤醒。CountDownLatch只能使用一次,不能重置。例如,在一个多线程计算任务中,主线程需要等待所有子线程计算完成后再进行汇总,就可以使用CountDownLatch。
    • CyclicBarrier:它可以让一组线程相互等待,直到所有线程都到达某个屏障点。它也有一个计数器,当所有线程都调用await方法时,计数器减到0,所有线程被唤醒,并且可以重置计数器,以便再次使用。例如,在一个多阶段的游戏中,每个玩家完成一个阶段后,需要等待所有玩家都完成,然后一起进入下一个阶段,就可以使用CyclicBarrier。
  4. Spring的IOC和AOP是什么
    • IOC(控制反转):是一种设计思想,将对象的创建和控制权从应用程序代码转移到Spring容器中。在传统的编程中,对象的创建和依赖关系的管理由开发者在代码中手动完成,而在Spring中,通过配置文件(如XML)或注解,将对象的创建、初始化和依赖注入交给Spring容器来管理。这样可以降低组件之间的耦合度,提高代码的可维护性和可测试性。例如,一个Service类依赖于一个Dao类,在Spring中可以通过配置或注解让Spring容器自动将Dao类的实例注入到Service类中。
    • AOP(面向切面编程):是一种编程范式,它将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,形成独立的切面。这些切面可以在不修改原有业务逻辑代码的情况下,通过动态代理等技术在运行时织入到目标对象的方法调用前后。例如,在一个电商系统中,为了记录每个订单操作的日志,可以创建一个日志切面,在订单相关方法执行前后记录日志信息。
  5. Spring Boot自动配置原理
    • Spring Boot的自动配置是基于条件配置(@Conditional)实现的。Spring Boot在启动时,会扫描所有的自动配置类(位于META - INF/spring.factories文件中定义的配置类)。
    • 每个自动配置类都使用了@Conditional注解及其相关的派生注解(如@ConditionalOnClass、@ConditionalOnProperty等)。这些注解会根据类路径下是否存在某些类、配置文件中是否存在某些属性等条件来决定是否生效。
    • 例如,当Spring Boot项目中引入了spring - data - jpa依赖时,Spring Data JPA的自动配置类会因为@ConditionalOnClass检查到相关JPA类存在而生效,从而自动配置数据源、EntityManagerFactory等相关组件。这样开发者只需要引入相关依赖,Spring Boot就能根据条件自动完成大部分的配置工作,极大地简化了Spring应用的开发。
  6. MyBatis的一级缓存和二级缓存有什么区别
    • 一级缓存:是SqlSession级别的缓存,在同一个SqlSession内有效。当执行查询操作时,MyBatis会先从一级缓存中查找,如果存在则直接返回,否则执行SQL查询并将结果放入一级缓存。一级缓存的作用域是SqlSession,当SqlSession关闭或提交事务时,一级缓存会被清空。例如,在同一个SqlSession中多次查询相同的SQL语句,只会执行一次数据库查询,后续直接从一级缓存获取结果。
    • 二级缓存:是Mapper级别的缓存,多个SqlSession可以共享二级缓存。二级缓存需要手动开启,并且需要实现Serializable接口(因为二级缓存可能会涉及到数据的序列化和反序列化)。当一个SqlSession查询数据时,如果一级缓存中没有,会先从二级缓存中查找。二级缓存的生命周期比一级缓存长,它在应用程序运行期间一直存在,除非手动清除。例如,不同的SqlSession查询相同的Mapper映射的SQL语句时,如果二级缓存中有数据,则直接从二级缓存获取,减少数据库查询压力。
  7. Dubbo的服务暴露和引用流程是怎样的
  • 服务暴露流程
    • 配置解析:Dubbo服务提供者启动时,会读取配置文件(如XML配置或注解配置),解析出服务接口、实现类、协议、端口等信息。
    • Proxy生成:通过ProxyFactory生成服务接口的代理对象,这个代理对象封装了服务的调用逻辑。
    • 协议绑定:根据配置的协议(如Dubbo协议、HTTP协议等),将服务绑定到指定的端口。例如,使用Dubbo协议时,会创建Netty服务器监听指定端口。
    • 注册中心注册:将服务的元数据(包括服务接口、版本、地址等信息)注册到注册中心(如Zookeeper、Nacos等)。注册中心用于服务的发现和管理,服务消费者可以从注册中心获取服务提供者的地址信息。
  • 服务引用流程
    • 配置解析:Dubbo服务消费者启动时,读取配置文件,解析出要引用的服务接口等信息。
    • 注册中心订阅:向注册中心订阅所需服务的地址信息。注册中心会将服务提供者的地址列表返回给服务消费者。
    • Proxy生成:通过ProxyFactory生成服务接口的代理对象,这个代理对象封装了服务的远程调用逻辑。
    • 负载均衡:当有多个服务提供者时,Dubbo会使用负载均衡策略(如随机、轮询、权重等)选择一个服务提供者进行调用。
    • 远程调用:根据配置的协议,通过网络通信(如Netty客户端)与服务提供者进行通信,发起远程方法调用,并获取调用结果。