面试官:请简要介绍一下 Java 核心知识中最基础且重要的部分。
王铁牛:Java 核心知识包括面向对象、数据类型、控制结构这些。比如面向对象的封装、继承、多态,能让代码更易维护和扩展。数据类型有基本类型和引用类型,控制结构像 if - else、for 循环这些是编程的基础。
面试官:还不错。那说说 JUC 包下的一些重要类及其作用。
王铁牛:JUC 里有 ExecutorService 线程池接口,能管理线程的创建、执行和销毁;还有 CountDownLatch 可以实现线程间的同步,比如多个线程等待某个条件达成。
面试官:嗯,回答得可以。接下来问几个关于 JVM 的问题,类加载机制分哪几个阶段?
王铁牛:类加载机制分加载、验证、准备、解析、初始化这几个阶段。加载就是把类的字节码文件加载到内存;验证确保字节码文件符合规范;准备为类的静态变量分配内存并赋初始值;解析把符号引用替换为直接引用;初始化就是执行类的静态代码块。
第一轮结束。
面试官:多线程中,如何保证线程安全?
王铁牛:可以用 synchronized 关键字,它能保证同一时刻只有一个线程访问被修饰的代码块或方法。还有 Lock 接口及其实现类,像 ReentrantLock 也能实现线程同步。
面试官:那线程池有哪些参数,分别有什么作用?
王铁牛:线程池有 corePoolSize(核心线程数),当提交的任务数小于它时,会创建新线程执行任务;maximumPoolSize(最大线程数),当任务数超过核心线程数时,会将任务放入队列,如果队列满了且线程数小于最大线程数,就会创建新线程执行任务;keepAliveTime(线程存活时间),当线程数大于核心线程数时,多余的线程在空闲时会存活的时间;unit(时间单位),就是 keepAliveTime 的时间单位;workQueue(任务队列),用来存放提交的任务;threadFactory(线程工厂),用于创建线程;handler(拒绝策略),当线程池满了且队列也满了,用来处理新提交任务的策略。
面试官:讲讲 HashMap 的底层实现原理。
王铁牛:HashMap 底层是数组 + 链表 + 红黑树。当往 HashMap 中 put 元素时,会先计算 key 的 hash 值,然后根据 hash 值找到对应的数组下标。如果该下标为空,就直接插入新节点;如果不为空,就遍历链表或红黑树,若 key 已存在则更新 value,若不存在则插入新节点。当链表长度大于 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查询效率。
第二轮结束。
面试官:Spring 框架中,依赖注入有哪些方式?
王铁牛:有构造器注入、setter 方法注入。构造器注入通过构造函数传入依赖对象;setter 方法注入是通过 set 方法设置依赖对象。
面试官:Spring Boot 自动配置原理了解吗?
王铁牛:Spring Boot 自动配置是通过 @EnableAutoConfiguration 注解实现的。它会根据类路径下的 jar 包、类等信息,自动配置 Spring 容器中的各种组件,比如数据源、事务管理器等。
面试官:MyBatis 的缓存机制是怎样的?
王铁牛:MyBatis 有一级缓存和二级缓存。一级缓存是 SqlSession 级别的,在同一个 SqlSession 中,查询相同数据时会直接从缓存中获取。二级缓存是 mapper 级别的,多个 SqlSession 可以共享二级缓存。当执行查询语句时,会先从一级缓存中查找,若没有则查询数据库,并将结果放入一级缓存。当 SqlSession 关闭时,一级缓存中的数据会刷新到二级缓存中。
面试官:最后问几个分布式相关的。Dubbo 服务调用的原理是什么?
王铁牛:Dubbo 服务调用是通过远程代理实现的。服务消费者通过代理对象调用远程服务,代理对象会将调用信息封装成请求,通过网络传输到服务提供者。服务提供者接收到请求后,根据请求信息调用具体的业务逻辑,并将结果返回给服务消费者。
第三轮结束。
面试官:今天的面试就到这里,回去等通知吧。
答案:
- Java 核心知识:
- 面向对象:
- 封装:将数据和操作数据的方法封装在一起,对外提供统一的接口,提高数据的安全性和代码的可维护性。例如一个类中有私有属性和公共的 get、set 方法来访问和修改私有属性。
- 继承:子类继承父类的属性和方法,实现代码复用。比如子类可以继承父类的成员变量和方法,避免重复编写。
- 多态:同一个行为具有多个不同表现形式或形态的能力。比如父类引用可以指向子类对象,调用同一个方法时会根据实际对象的类型执行不同的实现。
- 数据类型:
- 基本类型:包括 byte、short、int、long、float、double、char、boolean。它们直接存储值,占用固定大小的内存。
- 引用类型:如类、接口、数组等,存储的是对象的引用,指向对象在内存中的实际位置。
- 控制结构:
- if - else:根据条件判断执行不同的代码块。例如 if (条件) { 满足条件执行的代码 } else { 不满足条件执行的代码 }。
- for 循环:用于重复执行一段代码。格式为 for (初始化; 条件判断; 迭代) { 循环体 }。
- 面向对象:
- JUC 包下重要类及其作用:
- ExecutorService:
- 是线程池的接口。它能管理线程的创建、执行和销毁,提高线程的复用性,减少线程创建和销毁的开销。通过它可以方便地提交任务到线程池执行。例如 ExecutorService executor = Executors.newFixedThreadPool(5); 就创建了一个固定大小为 5 的线程池,后续可以通过 executor.submit(任务) 来提交任务执行。
- CountDownLatch:
- 用于实现线程间的同步。它有一个计数器,当一个或多个线程调用 countDown 方法时,计数器减 1。其他线程调用 await 方法会阻塞,直到计数器变为 0。比如多个线程等待某个任务完成后再继续执行,可以用 CountDownLatch。假设主线程要等待 3 个子线程都完成任务后再继续,就可以创建一个 CountDownLatch(3),子线程执行完任务后调用 countDown,主线程调用 await 方法等待。
- ExecutorService:
- JVM 类加载机制:
- 加载:
- 把类的字节码文件从硬盘等存储介质加载到内存中,形成一个 Class 对象。例如通过类加载器读取字节码文件,将其转换为 Class 对象。
- 验证:
- 确保字节码文件符合 JVM 的规范,防止恶意字节码的注入。比如检查字节码的格式是否正确、是否符合访问权限等。
- 准备:
- 为类的静态变量分配内存并赋初始值。比如静态 int 类型变量初始值为 0,静态引用类型变量初始值为 null。
- 解析:
- 把符号引用替换为直接引用。符号引用是一种间接引用,在解析阶段会将其转换为指向实际内存地址的直接引用。
- 初始化:
- 执行类的静态代码块,对静态变量进行赋值等操作。静态代码块会在类加载的初始化阶段执行。
- 加载:
- 多线程保证线程安全的方法:
- synchronized:
- 可以修饰代码块或方法。当一个线程访问被 synchronized 修饰的代码块或方法时,会先获取对象的锁。如果锁被其他线程持有,该线程会阻塞,直到锁被释放。例如 synchronized (对象) { 代码块 },这里的对象就是锁对象,多个线程访问该代码块时会竞争这个锁。
- Lock 接口及其实现类(如 ReentrantLock):
- Lock 接口提供了比 synchronized 更灵活的锁控制。例如 ReentrantLock 可以实现公平锁,通过 lock() 方法获取锁,unlock() 方法释放锁。还可以通过 tryLock() 方法尝试获取锁,避免死锁情况。比如在一些复杂的并发场景中,需要更精确地控制锁的获取和释放顺序,可以使用 ReentrantLock。
- synchronized:
- 线程池参数及其作用:
- corePoolSize(核心线程数):
- 当提交的任务数小于 corePoolSize 时,线程池会创建新线程来执行任务。例如一个任务队列容量有限,在任务量较小时,核心线程数能及时处理任务。
- maximumPoolSize(最大线程数):
- 当任务数超过 corePoolSize 且任务队列已满时,如果线程数小于 maximumPoolSize,会创建新线程执行任务。它限制了线程池最多能创建的线程数量。
- keepAliveTime(线程存活时间):
- 当线程数大于 corePoolSize 时,多余的线程在空闲时会存活 keepAliveTime 这么长时间,之后会被销毁。比如在业务低谷期,减少线程数量以节省资源。
- unit(时间单位):
- 就是 keepAliveTime 的时间单位,如 TimeUnit.SECONDS 表示秒。
- workQueue(任务队列):
- 用来存放提交的任务。常见的有 ArrayBlockingQueue、LinkedBlockingQueue 等。当任务提交速度大于核心线程数处理任务的速度时,任务会进入任务队列等待处理。
- threadFactory(线程工厂):
- 用于创建线程,通过它可以定制线程的属性,如线程名、优先级等。例如 new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("自定义线程名"); return t; } }。
- handler(拒绝策略):
- 当线程池满了且队列也满了,用来处理新提交任务的策略。常见的有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用者线程处理任务)、DiscardPolicy(丢弃新提交的任务)、DiscardOldestPolicy(丢弃队列中最旧的任务)。
- corePoolSize(核心线程数):
- HashMap 底层实现原理:
- HashMap 底层是数组 + 链表 + 红黑树。
- 计算 hash 值:
- 首先对 key 的 hashCode() 进行扰动处理,然后通过位运算得到一个比较均匀的 hash 值。例如通过 (h = key.hashCode()) ^ (h >>> 16) 进行扰动。
- 根据 hash 值找到数组下标:
- 通过 hash 值与数组长度减 1 进行按位与运算得到数组下标。即 index = hash & (table.length - 1)。
- 插入节点:
- 如果该下标为空,就直接插入新节点。如果不为空,就遍历链表或红黑树。若 key 已存在则更新 value,若不存在则插入新节点。当链表长度大于 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查询效率。
- Spring 框架中依赖注入的方式:
- 构造器注入:
- 通过构造函数传入依赖对象。例如 public class UserService { private final Dao dao; public UserService(Dao dao) { this.dao = dao; } },在创建 UserService 对象时,会通过构造函数传入 Dao 对象。
- setter 方法注入:
- 通过 set 方法设置依赖对象。比如 public class UserService { private Dao dao; public void setDao(Dao dao) { this.dao = dao; } },然后可以通过配置文件或注解的方式为 dao 属性注入具体的 Dao 对象。
- 构造器注入:
- Spring Boot 自动配置原理:
- Spring Boot 自动配置是通过 @EnableAutoConfiguration 注解实现的。
- 它会根据类路径下的 jar 包、类等信息,自动配置 Spring 容器中的各种组件。例如当类路径下有数据库相关的 jar 包时,会自动配置数据源、事务管理器等。它通过条件注解(如 @Conditional 及其衍生注解)来判断是否需要进行自动配置。比如只有在类路径下存在特定的数据库驱动时,才会配置相应的数据库连接组件。
- MyBatis 的缓存机制:
- 一级缓存:
- 是 SqlSession 级别的。在同一个 SqlSession 中,查询相同数据时会直接从缓存中获取。当执行查询语句时,会先从一级缓存中查找,若没有则查询数据库,并将结果放入一级缓存。当 SqlSession 关闭时,一级缓存中的数据会被清空。
- 二级缓存:
- 是 mapper 级别的,多个 SqlSession 可以共享二级缓存。当 SqlSession 关闭时,一级缓存中的数据会刷新到二级缓存中。二级缓存默认是关闭的,需要在 mapper 配置文件中开启。开启后,对于同一个 mapper 的查询,不同的 SqlSession 可以从二级缓存中获取数据。
- 一级缓存:
- Dubbo 服务调用原理:
- Dubbo 服务调用是通过远程代理实现的。
- 服务消费者通过代理对象调用远程服务,代理对象会将调用信息封装成请求,通过网络传输到服务提供者。服务提供者接收到请求后,根据请求信息调用具体的业务逻辑,并将结果返回给服务消费者。例如服务消费者有一个远程服务接口的代理对象,当调用代理对象的方法时,代理对象会将方法名、参数等信息封装成 Dubbo 请求,通过网络发送给服务提供者。服务提供者接收到请求后,找到对应的实现类执行方法,将结果再通过网络返回给服务消费者。