互联网大厂 Java 面试:核心知识、框架与中间件大考验
在互联网大厂的一间明亮面试室内,严肃的面试官正襟危坐,对面坐着略显紧张的求职者王铁牛。一场激烈的技术面试即将展开。
第一轮提问 面试官:“我们先从 Java 核心知识开始。能说一下 Java 中多态的实现方式有哪些吗?” 王铁牛:“多态的实现方式主要有两种,一种是方法重载,就是在一个类中可以有多个同名方法,但参数列表不同;另一种是方法重写,子类重写父类的方法。” 面试官:“回答得不错。那再问你,JVM 的内存区域是如何划分的?” 王铁牛:“JVM 内存主要分为堆、栈、方法区等。堆是存放对象实例的地方,栈主要是存储局部变量等,方法区存放类的信息、常量等。” 面试官:“很好。那说说多线程中 synchronized 关键字的作用和原理。” 王铁牛:“synchronized 关键字可以保证在同一时刻,只有一个线程可以访问被它修饰的代码块或方法。它的原理是通过对象头中的 Mark Word 来实现锁的状态标记。”
第二轮提问 面试官:“看来基础不错。我们接着聊聊 JUC。说说 CountDownLatch 和 CyclicBarrier 的区别。” 王铁牛:“嗯……这个嘛,好像它们都是用来协调线程的,具体区别我有点记不清了。” 面试官:“没关系,那再问你线程池的核心参数有哪些,分别代表什么含义。” 王铁牛:“有……有核心线程数,还有最大线程数,其他的我就不太确定了。” 面试官:“那说说 HashMap 在 JDK 1.7 和 JDK 1.8 中的区别。” 王铁牛:“好像 1.8 中结构有变化,具体怎么变的我也说不太清楚。”
第三轮提问 面试官:“我们再深入一点。谈谈 Spring 的 AOP 和 IOC 概念。” 王铁牛:“AOP 就是面向切面编程,IOC 是控制反转,但具体的实现和作用我讲不太明白。” 面试官:“那 Spring Boot 的自动配置原理是怎样的?” 王铁牛:“好像是根据依赖自动配置一些东西,具体的不太清楚。” 面试官:“最后问你,Dubbo 的集群容错策略有哪些?” 王铁牛:“我就知道有几种策略,但具体是哪些我不太记得了。”
面试官:“好了,今天的面试就到这里。你先回家等通知吧。通过刚才的面试,你的基础还算可以,对于一些简单的 Java 核心知识、多线程基础概念回答得比较准确,展现出了一定的知识储备。但在一些稍微复杂的知识点上,比如 JUC 工具类的区别、线程池参数细节、不同版本集合类的差异,以及 Spring、Spring Boot 等框架的深入原理和 Dubbo 的集群策略等方面,回答得不够理想,存在知识盲区或者理解不透彻的情况。后续你可以针对这些薄弱环节进行深入学习和巩固,提升自己的技术能力。我们会综合考虑所有面试者的情况,之后会给你反馈结果,希望你能耐心等待。”
问题答案
- Java 中多态的实现方式:
- 方法重载(Overloading):在一个类中可以有多个同名方法,但它们的参数列表(参数的类型、个数、顺序)不同。方法重载是编译时多态,编译器根据调用方法时传递的参数来决定调用哪个方法。例如:
class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
- **方法重写(Overriding)**:子类重写父类的方法,要求方法名、参数列表和返回值类型都相同(返回值类型可以是父类方法返回值类型的子类,称为协变返回类型)。方法重写是运行时多态,程序在运行时根据对象的实际类型来决定调用哪个类的方法。例如:
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
- JVM 的内存区域划分:
- 堆(Heap):是 JVM 中最大的一块内存区域,所有对象实例和数组都在堆上分配内存。堆是垃圾收集器管理的主要区域,根据对象的存活时间不同,堆又可以分为新生代、老年代等。
- 栈(Stack):包括虚拟机栈和本地方法栈。虚拟机栈用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法在执行时都会创建一个栈帧,栈帧入栈和出栈对应方法的调用和返回。本地方法栈则是为执行本地方法(用其他语言如 C、C++ 编写的方法)服务的。
- 方法区(Method Area):用于存储类的信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK 1.8 之前,方法区也被称为永久代,JDK 1.8 及以后,使用元空间(Metaspace)来替代永久代,元空间使用本地内存,而不是 JVM 内存。
- 程序计数器(Program Counter Register):可以看作是当前线程所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器,它是线程私有的。
- synchronized 关键字的作用和原理:
- 作用:synchronized 关键字用于实现线程同步,保证在同一时刻,只有一个线程可以访问被它修饰的代码块或方法。它可以防止多个线程同时访问共享资源而导致的数据不一致问题。
- 原理:在 Java 中,每个对象都有一个对象头,对象头中的 Mark Word 可以用来存储对象的锁状态信息。当一个线程访问被 synchronized 修饰的代码块或方法时,会先检查对象头的 Mark Word,如果该对象没有被锁定,线程会将 Mark Word 中的锁标志位设置为锁定状态,并将线程 ID 记录在 Mark Word 中,表示该线程已经获得了锁。其他线程想要访问该对象时,发现对象已经被锁定,就会进入阻塞状态,直到持有锁的线程释放锁。
- CountDownLatch 和 CyclicBarrier 的区别:
- CountDownLatch:是一个计数器递减的同步工具,它允许一个或多个线程等待其他线程完成操作。CountDownLatch 有一个初始计数器值,当一个线程完成任务后,会调用
countDown()方法将计数器减 1,当计数器的值为 0 时,等待的线程会被唤醒继续执行。CountDownLatch 的计数器只能使用一次,不能重置。例如,主线程等待多个子线程完成任务:
- CountDownLatch:是一个计数器递减的同步工具,它允许一个或多个线程等待其他线程完成操作。CountDownLatch 有一个初始计数器值,当一个线程完成任务后,会调用
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 模拟线程执行任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread finished");
latch.countDown();
}).start();
}
latch.await();
System.out.println("All threads finished");
}
}
- **CyclicBarrier**:是一个循环使用的同步屏障,它允许一组线程相互等待,直到所有线程都到达屏障点,然后所有线程再继续执行。CyclicBarrier 有一个初始的线程数量,当一个线程到达屏障点时,会调用 `await()` 方法,当所有线程都到达屏障点后,屏障会打开,所有线程可以继续执行。CyclicBarrier 可以重复使用,每次所有线程通过屏障后,计数器会重置。例如,多个线程完成一部分任务后,等待其他线程完成后再一起继续执行:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("All threads reached the barrier");
});
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 模拟线程执行任务
Thread.sleep(1000);
System.out.println("Thread reached the barrier");
barrier.await();
System.out.println("Thread continues after the barrier");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- 线程池的核心参数及含义:
- corePoolSize:核心线程数,线程池在初始化时会创建的线程数量,当提交的任务数小于核心线程数时,线程池会创建新的线程来执行任务。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当任务队列满了,且当前线程数小于最大线程数时,线程池会创建新的线程来执行任务。
- keepAliveTime:线程空闲时间,当线程池中的线程数量超过核心线程数时,空闲线程在等待新任务的时间超过 keepAliveTime 后会被销毁。
- TimeUnit:keepAliveTime 的时间单位,如 TimeUnit.SECONDS 表示秒。
- workQueue:任务队列,用于存储等待执行的任务。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。
- threadFactory:线程工厂,用于创建线程,可以自定义线程的名称、优先级等属性。
- RejectedExecutionHandler:拒绝策略,当任务队列满了,且线程数达到最大线程数时,新提交的任务会被拒绝,此时会调用拒绝策略来处理被拒绝的任务。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程来执行任务)等。
- HashMap 在 JDK 1.7 和 JDK 1.8 中的区别:
- 数据结构:
- JDK 1.7:HashMap 采用数组 + 链表的结构,当发生哈希冲突时,新的元素会插入到链表的头部(头插法)。
- JDK 1.8:HashMap 采用数组 + 链表 + 红黑树的结构,当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率。当红黑树中的节点数量小于 6 时,红黑树会转换为链表。
- 插入方式:
- JDK 1.7:采用头插法,插入新元素时会将新元素插入到链表的头部。这种方式在多线程环境下可能会导致链表成环的问题。
- JDK 1.8:采用尾插法,插入新元素时会将新元素插入到链表的尾部,避免了链表成环的问题。
- 哈希算法:
- JDK 1.7:哈希算法相对复杂,需要进行多次位运算和异或运算。
- JDK 1.8:哈希算法进行了优化,简化了计算过程,提高了性能。
- 数据结构:
- Spring 的 AOP 和 IOC 概念:
- AOP(面向切面编程):是一种编程范式,它允许开发者在不修改原有业务逻辑的基础上,对程序进行增强。AOP 将程序中的一些通用功能(如日志记录、事务管理等)提取出来,形成独立的切面,然后在需要的地方进行切入。AOP 的主要概念包括切面(Aspect)、通知(Advice)、连接点(Join Point)、切入点(Pointcut)等。例如,使用 AOP 实现日志记录:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("Method " + joinPoint.getSignature().getName() + " executed");
}
}
- **IOC(控制反转)**:也称为依赖注入(Dependency Injection),是一种设计原则,它将对象的创建和依赖关系的管理从代码中分离出来,交给 Spring 容器来完成。在传统的编程中,对象的创建和依赖关系的管理是由程序员手动完成的,而在 Spring 中,通过 IOC 容器可以自动创建和管理对象,对象之间的依赖关系通过配置文件或注解来声明。例如,使用注解实现依赖注入:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
- Spring Boot 的自动配置原理:
Spring Boot 的自动配置是基于 Spring 的条件注解和类路径扫描实现的。当 Spring Boot 应用启动时,会加载
META - INF/spring.factories文件,该文件中定义了一系列的自动配置类。Spring Boot 会根据类路径下的依赖和配置文件中的属性,使用条件注解(如@ConditionalOnClass、@ConditionalOnMissingBean等)来判断是否需要加载某个自动配置类。如果条件满足,就会将该自动配置类中的 Bean 定义注册到 Spring 容器中。例如,当类路径下存在 Tomcat 相关的类时,Spring Boot 会自动配置 Tomcat 作为嵌入式服务器。 - Dubbo 的集群容错策略:
- Failover Cluster:失败重试策略,当调用服务失败时,会自动重试其他服务器,默认重试 2 次。适用于读操作或幂等的写操作。
- Failfast Cluster:快速失败策略,当调用服务失败时,会立即抛出异常,不会进行重试。适用于非幂等的写操作。
- Failsafe Cluster:失败安全策略,当调用服务失败时,会忽略异常,返回一个空结果。适用于对结果要求不高的场景,如日志记录。
- Failback Cluster:失败自动恢复策略,当调用服务失败时,会记录失败的请求,然后在后台定时重试。适用于消息通知等场景。
- Forking Cluster:并行调用策略,会并行调用多个服务器,只要有一个服务器返回成功结果,就立即返回。适用于对实时性要求较高的场景。
- Broadcast Cluster:广播调用策略,会将请求广播到所有服务器,所有服务器都会执行请求。适用于系统配置更新等场景。