《互联网大厂Java求职者面试大揭秘:核心知识与实际应用的碰撞》

48 阅读8分钟

面试官:请简要介绍一下 Java 核心知识中面向对象的三大特性。

王铁牛:嗯,面向对象三大特性就是封装、继承和多态。封装就是把数据和操作数据的方法封装在一起;继承就是子类继承父类的属性和方法;多态就是同一个行为具有多个不同表现形式或形态。

面试官:不错,回答得挺清晰。那说说 JUC 包下常用的几个类及其作用。

王铁牛:像 CountDownLatch 可以用来控制一个或多个线程等待其他线程完成操作;CyclicBarrier 能让一组线程互相等待,直到到达某个公共屏障点;Semaphore 用于控制对共享资源的访问权限。

面试官:很好。进入第二轮,讲讲 JVM 的内存结构以及各部分的主要功能。

王铁牛:JVM 内存结构包括堆、栈、方法区等。堆是存放对象实例的地方;栈主要存放局部变量和方法调用的上下文;方法区存储类信息、常量、静态变量等。

面试官:还算基本正确。那多线程中如何创建线程,有几种方式?

王铁牛:有继承 Thread 类和实现 Runnable 接口两种方式,嗯,还有实现 Callable 接口。

面试官:最后一轮,说说线程池的工作原理和参数含义。

王铁牛:线程池工作原理就是预先创建一定数量的线程,当有任务来时从线程池中获取线程执行任务。参数嘛,corePoolSize 是核心线程数,maximumPoolSize 是最大线程数,keepAliveTime 是线程存活时间。

面试结束后,面试官表示会让王铁牛回家等通知,此次面试围绕 Java 核心知识、JUC、JVM、多线程、线程池等方面进行了三轮提问。王铁牛在一些简单问题上回答较好,得到了面试官的夸赞,但对于复杂问题回答得不够清晰准确。整体来看,王铁牛对基础知识有一定了解,但在深入理解和准确表述方面还有提升空间。通过这次面试,可以看出 Java 相关的这些知识点在实际求职中非常重要,需要求职者扎实掌握并能清晰阐述其原理和应用场景。

答案:

  1. 面向对象三大特性
    • 封装:将数据和操作数据的方法封装在一起,对外提供统一的接口。这样可以隐藏内部实现细节,提高代码的安全性和可维护性。例如,在一个类中定义私有属性和公共的访问方法,外部只能通过这些公共方法来访问和修改属性。
    • 继承:子类继承父类的属性和方法。通过继承可以实现代码复用,子类可以在父类的基础上进行扩展。比如,定义一个父类 Animal,子类 Dog 和 Cat 继承 Animal 类,它们就可以继承 Animal 的一些通用属性和方法,同时还能添加自己特有的属性和方法。
    • 多态:同一个行为具有多个不同表现形式或形态。多态的实现方式有两种,重载和重写。重载是指在同一个类中,方法名相同但参数列表不同;重写是指子类重写父类的方法。多态使得程序具有更好的扩展性和灵活性。例如,定义一个父类 Shape,子类 Circle 和 Rectangle 继承 Shape 类并重写 draw 方法,在使用时可以通过 Shape 类型的变量来调用不同子类的 draw 方法,实现不同的图形绘制。
  2. JUC 包下常用类及其作用
    • CountDownLatch:用于控制一个或多个线程等待其他线程完成操作。它有一个计数器,通过调用 countDown 方法来减少计数器的值,当计数器的值为 0 时,等待的线程会被唤醒。例如,在一个多线程任务中,主线程需要等待多个子线程完成任务后再继续执行,可以使用 CountDownLatch 来实现。
    • CyclicBarrier:能让一组线程互相等待,直到到达某个公共屏障点。它也有一个计数器,当每个线程调用 await 方法时,计数器减 1,当计数器的值为 0 时,所有等待的线程会被唤醒并继续执行。例如,在一个团队合作的任务中,多个成员需要完成各自的子任务,然后一起进行下一步操作,就可以使用 CyclicBarrier 来同步线程。
    • Semaphore:用于控制对共享资源的访问权限。它维护了一个许可证集合,通过 acquire 方法获取许可证,release 方法释放许可证。当许可证数量为 0 时,调用 acquire 方法的线程会被阻塞。例如,在一个多线程访问数据库的场景中,可以使用 Semaphore 来控制同时访问数据库的线程数量,防止数据库过载。
  3. JVM 内存结构及各部分主要功能
    • :是存放对象实例的地方,是 JVM 内存中最大的一块区域。所有的对象实例都在堆中分配内存。堆又可以分为新生代、老年代和永久代(在 JDK1.8 及以后,永久代被元空间取代)。新生代主要用于存放新创建的对象,老年代用于存放经过多次垃圾回收后仍然存活的对象,永久代(元空间)用于存储类信息、常量、静态变量等。
    • :主要存放局部变量和方法调用的上下文。每个线程都有自己独立的栈空间。当一个方法被调用时,会在栈中创建一个栈帧,用于存储该方法的局部变量、操作数栈、动态链接和方法返回地址等信息。
    • 方法区:存储类信息、常量、静态变量等。在 JDK1.8 及以后,方法区被元空间取代,元空间使用本地内存,而不是像永久代那样使用 JVM 堆内存。方法区中的数据对于多个线程是共享的。
  4. 多线程创建方式
    • 继承 Thread 类:定义一个类继承 Thread 类,并重写 run 方法,在 run 方法中编写线程执行的代码。然后通过创建该类的实例并调用 start 方法来启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("This is a thread created by extending Thread class");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
- **实现 Runnable 接口**:定义一个类实现 Runnable 接口,并重写 run 方法。然后通过创建 Thread 类的实例,并将实现了 Runnable 接口的类的实例作为参数传递给 Thread 类的构造函数,最后调用 start 方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("This is a thread created by implementing Runnable interface");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
- **实现 Callable 接口**:定义一个类实现 Callable 接口,并重写 call 方法。与 Runnable 接口的 run 方法不同,call 方法有返回值。然后通过创建 FutureTask 类的实例,并将实现了 Callable 接口的类的实例作为参数传递给 FutureTask 类的构造函数,再通过创建 Thread 类的实例,并将 FutureTask 类的实例作为参数传递给 Thread 类的构造函数,最后调用 start 方法启动线程。通过 FutureTask 的 get 方法可以获取 call 方法的返回值。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "This is a thread created by implementing Callable interface";
    }
}

public class Main {
    public static void main(String[] args) {
        MyCallable callable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            String result = futureTask.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 线程池工作原理和参数含义
    • 工作原理:线程池预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。如果线程池中的线程数量小于核心线程数,会创建新的线程来执行任务;如果线程数量达到核心线程数,任务会被放入任务队列中;如果任务队列已满,且线程数量小于最大线程数,会创建新的线程来执行任务;如果线程数量达到最大线程数,任务会根据拒绝策略进行处理。
    • 参数含义
      • corePoolSize:核心线程数。当提交的任务数小于 corePoolSize 时,线程池会创建新的线程来执行任务。
      • maximumPoolSize:最大线程数。当提交的任务数大于 corePoolSize 且任务队列已满时,线程池会创建新的线程来执行任务,直到线程数量达到 maximumPoolSize。
      • keepAliveTime:线程存活时间。当线程池中的线程数量大于 corePoolSize 时,多余的线程在空闲一段时间后会被销毁,这个空闲时间就是 keepAliveTime。
      • unit:keepAliveTime 的时间单位。
      • workQueue:任务队列。用于存放提交的任务,当线程池中的线程忙于执行任务时,新提交的任务会被放入任务队列中。
      • threadFactory:线程工厂。用于创建线程,通过自定义线程工厂可以设置线程的名称、优先级等属性。
      • handler:拒绝策略。当线程池中的线程数量达到 maximumPoolSize 且任务队列已满时,会调用拒绝策略来处理新提交的任务。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程处理任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。