《互联网大厂 Java 面试:核心知识、多线程、框架与中间件大考验》

34 阅读11分钟

互联网大厂 Java 面试:核心知识、多线程、框架与中间件大考验

在互联网大厂宽敞明亮的面试室内,严肃的面试官正襟危坐,对面坐着略显紧张的王铁牛。一场对 Java 核心知识、JUC、JVM 等多方面技术的考验即将拉开帷幕。

第一轮面试开始 面试官:首先问你几个关于 Java 核心知识的问题。Java 中基本数据类型有哪些? 王铁牛:这个我知道,有 byte、short、int、long、float、double、char、boolean。 面试官:不错,回答得很准确。那 Java 中重载和重写的区别是什么? 王铁牛:重载是在同一个类中,方法名相同但参数列表不同;重写是子类重写父类的方法,方法名、参数列表和返回值类型都要相同。 面试官:很好,基础很扎实。那 final、finally 和 finalize 的区别呢? 王铁牛:final 可以修饰类、方法和变量,修饰类时类不能被继承,修饰方法时方法不能被重写,修饰变量时变量变成常量;finally 一般用在 try-catch 语句中,无论是否发生异常,finally 块中的代码都会执行;finalize 是 Object 类的一个方法,在对象被垃圾回收之前会调用这个方法。 面试官:非常棒,看来你对 Java 核心知识掌握得很牢固。

第二轮面试开始 面试官:接下来聊聊 JUC 和多线程。什么是线程安全? 王铁牛:线程安全就是在多线程环境下,对共享资源的访问不会出现数据不一致等问题。 面试官:回答正确。那 Java 中实现多线程有哪些方式? 王铁牛:有继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。 面试官:很好。那 JUC 中的 CountDownLatch 是做什么用的? 王铁牛:呃……这个……好像是和线程同步有关吧,具体我有点不太清楚。 面试官:这个是用来让一个或多个线程等待其他线程完成操作的工具类。那再问你,线程池的核心参数有哪些? 王铁牛:这个我知道,有核心线程数、最大线程数、线程空闲时间、时间单位、任务队列和拒绝策略。 面试官:不错,大部分还是掌握得挺好的。

第三轮面试开始 面试官:现在问你一些关于框架和中间件的问题。Spring 的核心特性有哪些? 王铁牛:有依赖注入和面向切面编程。 面试官:很好。那 Spring Boot 是如何实现自动配置的? 王铁牛:呃……好像是通过一些配置文件和注解吧,具体我也说不太清楚。 面试官:Spring Boot 是通过自动配置类、条件注解和 SPI 机制来实现自动配置的。那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:这个我知道,#{} 是预编译处理,{} 是字符串替换。 面试官:对的。那 Dubbo 的负载均衡策略有哪些? 王铁牛:嗯……好像有随机、轮询什么的,其他的我就不太确定了。 面试官:Dubbo 主要有随机、轮询、最少活跃调用数、一致性哈希这几种负载均衡策略。最后问你,Redis 的持久化方式有哪些? 王铁牛:好像有 RDB 和 AOF,具体原理我不太清楚。

面试官:整体来看,你对一些基础的 Java 知识掌握得还可以,但对于一些稍微复杂的 JUC、框架和中间件的原理和细节了解得不够深入。你先回家等通知吧,后续如果有进一步的消息会及时联系你。

问题答案详细解析

  1. Java 中基本数据类型有哪些?
    • Java 中有 8 种基本数据类型,分为 4 类:
      • 整数类型:byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)。
      • 浮点类型:float(4 字节)、double(8 字节)。
      • 字符类型:char(2 字节)。
      • 布尔类型:boolean(理论上没有明确大小,实际根据 JVM 实现)。
  2. Java 中重载和重写的区别是什么?
    • 重载(Overloading):发生在同一个类中,方法名相同,但参数列表不同(参数的类型、个数、顺序不同),与方法的返回值类型和访问修饰符无关。例如:
public class OverloadExample {
    public int add(int a, int b) {
        return a + b;
    }
    public double add(double a, double b) {
        return a + b;
    }
}
- 重写(Overriding):发生在子类和父类之间,子类重写父类的方法,方法名、参数列表和返回值类型都要相同,访问修饰符不能比父类的更严格,抛出的异常不能比父类的更宽泛。例如:
class Parent {
    public void print() {
        System.out.println("Parent");
    }
}
class Child extends Parent {
    @Override
    public void print() {
        System.out.println("Child");
    }
}
  1. final、finally 和 finalize 的区别呢?
    • final:
      • 修饰类:该类不能被继承,例如 final class FinalClass {}
      • 修饰方法:该方法不能被重写,例如 public final void finalMethod() {}
      • 修饰变量:该变量变成常量,一旦赋值就不能再修改,例如 final int CONSTANT = 10;
    • finally:通常与 try-catch 语句一起使用,无论 try 块中是否发生异常,finally 块中的代码都会执行。例如:
try {
    // 可能会抛出异常的代码
} catch (Exception e) {
    // 异常处理代码
} finally {
    // 无论是否发生异常都会执行的代码
}
- finalize:是 Object 类的一个方法,当对象被垃圾回收之前,垃圾回收器会调用该对象的 finalize 方法。但不建议使用该方法进行资源释放等操作,因为它的执行时间不确定。

4. 什么是线程安全? 线程安全是指在多线程环境下,对共享资源的访问不会出现数据不一致、脏读、幻读等问题。例如,多个线程同时对一个共享的变量进行读写操作,如果没有进行适当的同步控制,就可能会出现线程安全问题。为了保证线程安全,可以使用同步机制,如 synchronized 关键字、Lock 接口等。 5. Java 中实现多线程有哪些方式? - 继承 Thread 类:创建一个类继承自 Thread 类,重写 run 方法,然后创建该类的对象并调用 start 方法启动线程。例如:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
- 实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run 方法,然后将该类的对象作为参数传递给 Thread 类的构造函数,再调用 start 方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running");
    }
}
public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
- 实现 Callable 接口:创建一个类实现 Callable 接口,实现 call 方法,该方法有返回值。然后使用 FutureTask 包装 Callable 对象,再将 FutureTask 对象作为参数传递给 Thread 类的构造函数,调用 start 方法启动线程,最后可以通过 FutureTask 的 get 方法获取线程执行的结果。例如:
import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 1 + 2;
    }
}
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        Integer result = futureTask.get();
        System.out.println("Result: " + result);
    }
}
- 使用线程池:通过 Executors 工具类创建不同类型的线程池,将任务提交给线程池执行。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.submit(() -> {
            System.out.println("Task is running");
        });
        executorService.shutdown();
    }
}
  1. JUC 中的 CountDownLatch 是做什么用的? CountDownLatch 是一个同步工具类,它可以让一个或多个线程等待其他线程完成操作。它通过一个计数器来实现,初始化时设置计数器的值,当一个线程完成任务后,调用 countDown 方法将计数器减 1,当计数器的值为 0 时,等待的线程会被唤醒继续执行。例如:
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟线程执行任务
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " finished");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        // 主线程等待其他线程完成
        countDownLatch.await();
        System.out.println("All threads finished");
    }
}
  1. 线程池的核心参数有哪些?
    • 核心线程数(corePoolSize):线程池中长期保持的线程数量,当提交的任务数小于核心线程数时,线程池会创建新的线程来执行任务。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量,当任务队列满了且线程数小于最大线程数时,线程池会创建新的线程来执行任务。
    • 线程空闲时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,多余的空闲线程在等待新任务的最长时间,超过这个时间线程会被销毁。
    • 时间单位(unit):线程空闲时间的时间单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
    • 任务队列(workQueue):用于存放待执行任务的队列,常见的有 ArrayBlockingQueue、LinkedBlockingQueue 等。
    • 拒绝策略(handler):当任务队列满了且线程数达到最大线程数时,新提交的任务会被拒绝,这时会根据拒绝策略进行处理。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(让提交任务的线程执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
  2. Spring 的核心特性有哪些?
    • 依赖注入(Dependency Injection,DI):是一种设计模式,通过外部容器来管理对象之间的依赖关系。例如,一个类需要另一个类的实例,可以通过构造函数、setter 方法等方式将依赖的对象注入进来,而不是在类内部直接创建。这样可以降低类之间的耦合度,提高代码的可维护性和可测试性。
    • 面向切面编程(Aspect-Oriented Programming,AOP):允许在不修改原有代码的基础上,对程序进行增强。例如,在方法执行前后添加日志记录、事务管理等功能。AOP 通过切面(Aspect)、切点(Pointcut)、通知(Advice)等概念来实现。
  3. Spring Boot 是如何实现自动配置的? Spring Boot 的自动配置主要通过以下几个机制实现:
    • 自动配置类:Spring Boot 提供了大量的自动配置类,这些类位于 spring-boot-autoconfigure 模块中,它们使用 @Configuration 注解标记,用于配置各种组件。
    • 条件注解:自动配置类中使用了大量的条件注解,如 @ConditionalOnClass、@ConditionalOnMissingBean 等,这些注解会根据类路径中是否存在某个类、是否已经存在某个 Bean 等条件来决定是否进行自动配置。
    • SPI 机制:Spring Boot 通过 Java 的 SPI(Service Provider Interface)机制,在 META-INF/spring.factories 文件中配置了大量的自动配置类,当 Spring Boot 启动时,会读取该文件并加载相应的自动配置类。
  4. MyBatis 中 #{} 和 ${} 的区别是什么?
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ? 占位符,然后使用 PreparedStatement 进行预编译,这样可以防止 SQL 注入攻击。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
- ${}:是字符串替换,MyBatis 在处理 ${} 时,会将 ${} 直接替换为传入的参数值,可能会导致 SQL 注入攻击。例如:
<select id="getUserByUsername" parameterType="String" resultType="User">
    SELECT * FROM users WHERE username = '${username}'
</select>
  1. Dubbo 的负载均衡策略有哪些?
    • 随机(RandomLoadBalance):随机选择一个服务提供者,默认的负载均衡策略。
    • 轮询(RoundRobinLoadBalance):按照顺序依次选择服务提供者。
    • 最少活跃调用数(LeastActiveLoadBalance):选择当前活跃调用数最少的服务提供者,如果有多个服务提供者的活跃调用数相同,则随机选择一个。
    • 一致性哈希(ConsistentHashLoadBalance):根据请求的参数计算哈希值,将请求路由到固定的服务提供者,适合缓存等场景。
  2. Redis 的持久化方式有哪些?
    • RDB(Redis Database):是 Redis 的默认持久化方式,它会在指定的时间间隔内将内存中的数据快照保存到磁盘上。RDB 持久化方式的优点是文件紧凑,恢复速度快;缺点是可能会丢失最后一次快照之后的数据。可以通过配置文件中的 save 指令来设置快照的时间间隔,例如 save 900 1 表示在 900 秒内至少有 1 个键发生变化时,就进行一次快照。
    • AOF(Append Only File):以日志的形式记录 Redis 的写操作,将每个写命令追加到文件末尾。AOF 持久化方式的优点是数据安全性高,几乎不会丢失数据;缺点是文件体积较大,恢复速度相对较慢。可以通过配置文件中的 appendonly yes 来开启 AOF 持久化,还可以通过 appendfsync 配置项设置同步策略,如 always(每次写操作都同步)、everysec(每秒同步一次)、no(由操作系统决定何时同步)。