《互联网大厂面试:Java核心知识、JUC、JVM等全方位考察》

99 阅读2分钟

《互联网大厂面试:Java核心知识、JUC、JVM等全方位考察》

在互联网大厂的一间安静的面试室内,严肃的面试官正襟危坐,对面坐着略显紧张的王铁牛。面试开始,一场关于Java技术知识的考验拉开帷幕。

第一轮提问

  • 面试官:首先问几个基础的Java核心知识问题。Java中基本数据类型有哪些?
  • 王铁牛:嗯,有byte、short、int、long、float、double、char、boolean。
  • 面试官:回答正确。那Java中多态的实现方式有哪些?
  • 王铁牛:主要有继承和接口实现,通过方法重写和方法重载来体现多态。
  • 面试官:非常好。那说说String、StringBuilder和StringBuffer的区别。
  • 王铁牛:String是不可变的,每次对String的操作都会生成新的对象;StringBuilder是非线程安全的,性能较高;StringBuffer是线程安全的,性能相对较低。
  • 面试官:不错,基础很扎实。

第二轮提问

  • 面试官:接下来考察一下JUC和多线程方面的知识。什么是线程池?
  • 王铁牛:线程池就是预先创建好一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。
  • 面试官:很好。那线程池有哪些常用的创建方式?
  • 王铁牛:可以通过Executors工厂类创建,比如newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor等。
  • 面试官:那说说线程池的工作流程。
  • 王铁牛:呃……就是有任务来了就执行呗,执行完就释放线程。(回答不清晰)
  • 面试官:这个回答不太准确,需要更深入理解。再问一个,JUC中常用的同步工具类有哪些?
  • 王铁牛:嗯……好像有CountDownLatch、CyclicBarrier,其他的不太记得了。

第三轮提问

  • 面试官:现在考察框架相关知识。Spring中Bean的生命周期是怎样的?
  • 王铁牛:呃……就是创建、初始化,然后销毁吧。(回答不清晰)
  • 面试官:这样回答太笼统了。那Spring Boot的自动配置原理是什么?
  • 王铁牛:好像是根据依赖自动配置一些东西,具体不太清楚。(回答不清晰)
  • 面试官:那MyBatis中#{}和${}的区别是什么?
  • 王铁牛:嗯……好像一个安全,一个不安全,但具体不太明白。(回答不清晰)
  • 面试官:看来你对框架的理解还不够深入。

面试接近尾声,面试官表情严肃地说:“今天的面试就到这里,你先回家等通知吧。我们后续会综合评估你的表现,有结果会及时通知你。”

问题答案详细解析

  1. Java中基本数据类型有哪些?
    • Java中有8种基本数据类型,分为4类:
      • 整数类型:byte(1字节)、short(2字节)、int(4字节)、long(8字节)。
      • 浮点类型:float(4字节)、double(8字节)。
      • 字符类型:char(2字节)。
      • 布尔类型:boolean(理论上没有明确大小,实际实现中通常是1字节)。
  2. Java中多态的实现方式有哪些?
    • 多态是指同一个行为具有多个不同表现形式或形态的能力。Java中多态的实现方式主要有:
      • 继承:子类继承父类,重写父类的方法,通过父类引用指向子类对象,调用重写的方法实现多态。例如:
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");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.sound();
    }
}
    - 接口实现:类实现接口,重写接口中的方法,通过接口引用指向实现类对象,调用重写的方法实现多态。
    - 方法重载:在同一个类中,方法名相同,但参数列表不同(参数个数、类型、顺序不同),根据调用时传入的参数不同来调用不同的方法。

3. String、StringBuilder和StringBuffer的区别 - String:是不可变的,一旦创建,其值不能被修改。每次对String进行操作(如拼接、替换等)都会创建一个新的String对象,因此频繁操作String会产生大量的临时对象,影响性能。 - StringBuilder:是可变的,非线程安全的。它的内部使用可变的字符数组来存储字符串,在进行字符串拼接等操作时,不会创建新的对象,而是直接在原数组上进行修改,因此性能较高。适用于单线程环境下的字符串操作。 - StringBuffer:也是可变的,但它是线程安全的。它的方法大多使用了synchronized关键字进行同步,保证了在多线程环境下操作的安全性,但由于同步会带来一定的性能开销,所以性能相对StringBuilder较低。适用于多线程环境下的字符串操作。 4. 什么是线程池? - 线程池是一种线程使用模式。线程池预先创建好一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。任务执行完后,线程不会销毁,而是返回线程池,等待下一个任务。使用线程池可以减少线程创建和销毁的开销,提高系统的性能和资源利用率,同时可以更好地管理线程的数量,避免创建过多线程导致系统资源耗尽。 5. 线程池有哪些常用的创建方式? - Java中可以通过Executors工厂类创建线程池,常用的创建方式有: - newFixedThreadPool:创建一个固定大小的线程池,线程池中的线程数量固定不变。当有新任务提交时,如果线程池中有空闲线程,则立即执行任务;如果没有空闲线程,则任务会被放入队列中等待。例如:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
    - newCachedThreadPool:创建一个可缓存的线程池,线程池中的线程数量可以根据需要动态调整。如果有新任务提交,而线程池中没有空闲线程,则会创建新的线程来执行任务;如果线程在一段时间内没有被使用,则会被销毁。例如:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    - newSingleThreadExecutor:创建一个单线程的线程池,线程池中只有一个线程。所有任务会按照提交的顺序依次执行。例如:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  1. 线程池的工作流程
    • 当有新任务提交到线程池时,线程池的工作流程如下:
      • 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下一步。
      • 线程池判断工作队列是否已经满了。如果工作队列没有满,则将新任务存储在这个工作队列里。如果工作队列满了,则进入下一步。
      • 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
      • 常见的饱和策略有:AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程处理该任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试执行新任务)。
  2. JUC中常用的同步工具类有哪些?
    • JUC(java.util.concurrent)是Java提供的并发包,其中常用的同步工具类有:
      • CountDownLatch:允许一个或多个线程等待其他线程完成操作。它通过一个计数器来实现,计数器的初始值为线程的数量。当一个线程完成任务后,计数器的值减1,当计数器的值为0时,等待的线程可以继续执行。例如:
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " is working");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " has finished");
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println("All threads have finished, main thread can continue");
    }
}
    - CyclicBarrier:允许一组线程相互等待,直到所有线程都到达一个屏障点,然后所有线程可以继续执行。它可以重复使用,当所有线程都到达屏障点后,屏障会被重置。例如:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("All threads have reached the barrier");
        });
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " is working");
                try {
                    Thread.sleep(1000);
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " has passed the barrier");
            }).start();
        }
    }
}
    - Semaphore:用于控制同时访问某个资源的线程数量。它通过一个许可证来实现,线程在访问资源前需要获取许可证,访问完后释放许可证。例如:
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " has acquired the semaphore");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " has released the semaphore");
                }
            }).start();
        }
    }
}
  1. Spring中Bean的生命周期是怎样的?
    • Spring中Bean的生命周期包括以下几个主要阶段:
      • 实例化:通过反射创建Bean的实例。
      • 属性赋值:为Bean的属性注入值。
      • 初始化:
        • 实现InitializingBean接口的afterPropertiesSet方法。
        • 自定义的init-method方法。
      • 使用:Bean可以被使用。
      • 销毁:
        • 实现DisposableBean接口的destroy方法。
        • 自定义的destroy-method方法。
    • 例如,定义一个实现InitializingBean和DisposableBean接口的Bean:
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class MyBean implements InitializingBean, DisposableBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("MyBean is initializing");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("MyBean is destroying");
    }
}
  1. Spring Boot的自动配置原理是什么?
    • Spring Boot的自动配置是基于条件注解和类路径扫描实现的。其核心原理如下:
      • Spring Boot启动时,会通过@SpringBootApplication注解开启自动配置功能,该注解包含了@EnableAutoConfiguration注解。
      • @EnableAutoConfiguration注解会导入AutoConfigurationImportSelector类,该类会读取META-INF/spring.factories文件,该文件中定义了所有的自动配置类。
      • Spring Boot会根据类路径中存在的类和配置文件中的属性,使用条件注解(如@ConditionalOnClass、@ConditionalOnMissingBean等)来判断是否需要加载某个自动配置类。如果满足条件,则会加载该自动配置类,为应用程序自动配置相应的Bean。
      • 例如,当类路径中存在DataSource类时,Spring Boot会自动配置数据源相关的Bean。
  2. MyBatis中#{}和${}的区别是什么?
    • #{}:是预编译处理,MyBatis会将#{}替换为占位符?,然后使用PreparedStatement进行参数设置。这样可以防止SQL注入攻击,因为参数会被自动进行类型转换和转义处理。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
- ${}:是字符串替换,MyBatis会直接将${}替换为传入的参数值。这种方式存在SQL注入风险,因为参数不会进行任何处理,直接拼接到SQL语句中。例如:
<select id="getUserByColumnName" parameterType="String" resultType="User">
    SELECT * FROM users WHERE ${columnName} = 'value'
</select>
- 一般情况下,尽量使用#{}来防止SQL注入,只有在需要动态传入表名、列名等情况时才使用${},但要确保传入的参数是安全的。