面试官:请简要介绍一下 Java 核心知识中的面向对象编程概念,以及它在实际业务场景中的应用。
王铁牛:面向对象编程主要有封装、继承和多态。封装就是把数据和操作数据的方法封装在一起;继承是子类继承父类的属性和方法;多态就是同一个行为具有多个不同表现形式。在实际业务中,比如开发一个电商系统,商品类可以封装商品的属性和操作方法,不同类型的商品可以继承商品类,并且根据多态性,在不同场景下可以有不同的表现。
面试官:嗯,回答得不错。那说说 JUC 包中的 CountDownLatch 是如何使用的,在什么业务场景下会用到它?
王铁牛:CountDownLatch 就是一个计数器,调用 countDown 方法计数器减一,调用 await 方法会阻塞直到计数器为 0。比如说,一个业务场景是多个线程需要完成各自的任务后,再一起执行后续操作,就可以用 CountDownLatch 来实现。
面试官:很好。再问你,JVM 的内存模型包括哪些部分,各部分的作用是什么?
王铁牛:JVM 内存模型包括程序计数器、虚拟机栈、本地方法栈、堆和方法区。程序计数器记录当前线程执行的字节码指令地址;虚拟机栈存放局部变量表、操作数栈等;本地方法栈用于执行本地方法;堆是对象实例的存储区域;方法区存储类信息、常量等。
面试官:第一轮面试结束。接下来问你多线程相关的问题。说说如何创建一个线程,并且如何让多个线程交替执行?
王铁牛:创建线程可以继承 Thread 类或者实现 Runnable 接口。要让多个线程交替执行,可以用 wait 和 notify 方法。
面试官:那如何保证多线程环境下数据的一致性和安全性呢?
王铁牛:可以用 synchronized 关键字或者 Lock 接口来加锁,还有使用线程安全的集合类。
面试官:线程池有哪些参数,它们的作用分别是什么?
王铁牛:线程池有 corePoolSize、maximumPoolSize、keepAliveTime、unit 和 workQueue 等参数。corePoolSize 是核心线程数,maximumPoolSize 是最大线程数,keepAliveTime 是线程空闲时的存活时间,unit 是时间单位,workQueue 是任务队列。
面试官:第二轮面试结束。现在问你关于 HashMap 的问题。HashMap 的底层数据结构是什么,它是如何实现键值对存储的?
王铁牛:HashMap 底层是数组 + 链表 + 红黑树。当哈希值冲突时,会在链表或红黑树中存储键值对。
面试官:那 HashMap 在多线程环境下会出现什么问题,如何解决?
王铁牛:会出现数据丢失、死循环等问题。可以用 ConcurrentHashMap 来替代。
面试官:ArrayList 是线程安全的吗,如果不是,如何保证线程安全?
王铁牛:ArrayList 不是线程安全的。可以用 Collections.synchronizedList 方法将其变成线程安全的,或者使用 CopyOnWriteArrayList。
面试官:第三轮面试结束。接下来问你 Spring 相关的问题。Spring 框架的核心特性有哪些,它是如何实现依赖注入的?
王铁牛:Spring 核心特性有依赖注入、面向切面编程等。依赖注入通过构造器注入、setter 方法注入等方式实现。
面试官:Spring Boot 与 Spring 相比,有哪些优势?
王铁牛:Spring Boot 简化了配置,内置了很多依赖,能快速搭建项目。
面试官:说说 MyBatis 的工作原理,以及它的核心组件有哪些?
王铁牛:MyBatis 先读取配置文件,通过 SqlSessionFactory 创建 SqlSession,再由 SqlSession 执行 SQL 语句。核心组件有 SqlSessionFactory、SqlSession、Mapper 接口等。
面试官:面试结束了。回去等通知吧。
答案:
- Java 面向对象编程概念及应用:
- 封装:将数据和操作数据的方法封装在一起,对外提供统一的访问接口。例如在电商系统中,商品类封装商品的属性(如名称、价格、库存等)和操作方法(如计算总价、检查库存等),外部代码通过调用商品类的公开方法来操作商品数据,而不需要了解商品类内部的数据存储和操作细节,提高了数据的安全性和程序的可维护性。
- 继承:子类继承父类的属性和方法。在电商系统中,不同类型的商品(如电子产品、服装等)可以继承商品类,继承商品类的通用属性(如名称、价格)和方法(如展示商品信息),同时可以根据自身特点添加特殊的属性和方法,实现代码的复用和扩展。
- 多态:同一个行为具有多个不同表现形式。在电商系统中,商品类有一个展示商品信息的方法,不同类型的商品继承该方法后,可以根据自身特点以不同的方式展示商品信息,体现了多态性。
- JUC 包中 CountDownLatch 的使用及业务场景:
- 使用方法:CountDownLatch 是一个计数器,通过调用 countDown 方法使计数器减一,调用 await 方法会阻塞当前线程,直到计数器的值变为 0。例如,假设有一个任务需要多个子任务完成后才能继续执行,我们可以创建一个 CountDownLatch,在每个子任务完成时调用 countDown 方法,在主任务中调用 await 方法,这样主任务就会等待所有子任务完成后再继续执行。
- 业务场景:比如在一个电商促销活动中,需要多个系统模块(如库存系统、订单系统、支付系统等)都完成各自的准备工作后,才能正式启动促销活动。这时就可以使用 CountDownLatch,每个系统模块完成准备工作后调用 countDown 方法,当所有系统模块都完成后,主线程调用 await 方法后的代码才会执行,从而启动促销活动。
- JVM 内存模型各部分的作用:
- 程序计数器:记录当前线程执行的字节码指令地址,是线程私有的,每个线程都有自己独立的程序计数器。它的作用是保证线程能够准确地执行下一条指令,在多线程环境下,不同线程的程序计数器互不影响。
- 虚拟机栈:存放局部变量表、操作数栈等。局部变量表用于存储方法中的局部变量,操作数栈用于执行字节码指令时进行数据的操作。虚拟机栈是线程私有的,每个线程都有自己的虚拟机栈,当方法执行结束后,相应的栈帧会被弹出。
- 本地方法栈:与虚拟机栈类似,用于执行本地方法(用 C、C++ 等本地语言编写的方法)。
- 堆:是对象实例的存储区域,所有的对象实例都在堆中分配内存。堆是线程共享的,是垃圾回收的主要区域。
- 方法区:存储类信息、常量、静态变量等。方法区也是线程共享的,在 Java 8 及以后的版本中,方法区被元空间(MetaSpace)所取代,元空间使用本地内存,不再受限于 JVM 的堆内存大小。
- 多线程创建及交替执行方法:
- 创建线程:
- 继承 Thread 类:定义一个类继承 Thread 类,并重写 run 方法,在 run 方法中编写线程要执行的代码。例如:
- 创建线程:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("This is a thread created by extending Thread class");
}
}
// 使用时:MyThread thread = new MyThread(); thread.start();
- **实现 Runnable 接口**:定义一个类实现 Runnable 接口,并重写 run 方法,然后通过 Thread 类的构造函数将实现了 Runnable 接口的对象作为参数传入,创建线程对象。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("This is a thread created by implementing Runnable interface");
}
}
// 使用时:Thread thread = new Thread(new MyRunnable()); thread.start();
- **线程交替执行**:可以使用 wait 和 notify 方法来实现。例如,有两个线程 A 和 B,需要交替执行:
class AlternateThread {
private static final Object lock = new Object();
private static boolean flag = true;
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized (lock) {
while (flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread A");
flag = true;
lock.notify();
}
});
Thread threadB = new Thread(() -> {
synchronized (lock) {
while (!flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread B");
flag = false;
lock.notify();
}
});
threadA.start();
threadB.start();
}
}
在上述代码中,通过一个共享的锁对象 lock 和一个标志位 flag 来控制两个线程的交替执行。线程 A 在 flag 为 true 时等待,线程 B 在 flag 为 false 时等待,当一个线程执行完后,修改 flag 并唤醒另一个线程。
- 多线程环境下数据一致性和安全性保证方法:
- synchronized 关键字:可以修饰方法或代码块,被修饰的方法或代码块在同一时刻只能被一个线程访问。例如:
public synchronized void method() {
// 方法体中的代码在同一时刻只能被一个线程执行
}
或者
synchronized (object) {
// 代码块中的代码在同一时刻只能被一个线程执行
}
- **Lock 接口**:如 ReentrantLock,提供了比 synchronized 更灵活的锁控制。可以使用 lock() 方法获取锁,unlock() 方法释放锁,还可以实现公平锁等特性。例如:
private static final Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 方法体中的代码在同一时刻只能被一个线程执行
} finally {
lock.unlock();
}
}
- **线程安全的集合类**:如 CopyOnWriteArrayList,在读取操作时不需要加锁,写入操作时会复制一份新的数组,修改完成后再将新数组替换原数组,保证了读取操作的线程安全性。
- 线程池参数及作用:
- corePoolSize:核心线程数。当提交的任务数小于 corePoolSize 时,线程池会创建新的线程来执行任务。
- maximumPoolSize:最大线程数。当提交的任务数大于 corePoolSize 且任务队列已满时,线程池会创建新的线程来执行任务,直到线程数达到 maximumPoolSize。
- keepAliveTime:线程空闲时的存活时间。当线程数大于 corePoolSize 且线程空闲时间超过 keepAliveTime 时,多余的线程会被销毁。
- unit:keepAliveTime 的时间单位,如 TimeUnit.SECONDS 表示秒。
- workQueue:任务队列,用于存放提交的任务。当提交的任务数大于 corePoolSize 时,任务会被放入任务队列中等待执行。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。
- HashMap 底层数据结构及键值对存储实现:
- 底层数据结构:HashMap 底层是数组 + 链表 + 红黑树。初始时,HashMap 有一个默认大小的数组,当向 HashMap 中插入键值对时,会根据键的哈希值计算出在数组中的位置。
- 键值对存储实现:如果该位置为空,则直接插入新的键值对。如果该位置不为空,说明发生了哈希冲突,此时会将新的键值对存储在链表或红黑树中。当链表长度超过一定阈值(默认 8)且数组容量大于等于 64 时,链表会转换为红黑树,以提高查询效率。
- HashMap 在多线程环境下的问题及解决方法:
- 问题:
- 数据丢失:在扩容时,如果多个线程同时进行插入操作,可能会导致链表形成环形结构,从而在后续查询时导致数据丢失。
- 死循环:同样在扩容时,由于多线程操作可能会导致链表节点的顺序错乱,进而在遍历链表时形成死循环。
- 解决方法:使用 ConcurrentHashMap 替代 HashMap。ConcurrentHashMap 在多线程环境下是线程安全的,它通过分段锁等机制来保证数据的一致性和安全性。
- 问题:
- ArrayList 线程安全性及保证方法:
- 线程安全性:ArrayList 不是线程安全的。在多线程环境下,同时对 ArrayList 进行读写操作可能会导致数据不一致等问题。
- 保证方法:
- 使用 Collections.synchronizedList:将 ArrayList 包装成线程安全的 List。例如:
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
- **使用 CopyOnWriteArrayList**:CopyOnWriteArrayList 是一个线程安全的 List,在读取操作时不需要加锁,写入操作时会复制一份新的数组,修改完成后再将新数组替换原数组,保证了读取操作的线程安全性。例如:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
- Spring 框架核心特性及依赖注入实现方式:
- 核心特性:
- 依赖注入:通过控制反转(IoC)实现,将对象的创建和依赖关系的管理交给 Spring 容器,而不是在对象内部自行创建和管理依赖对象。
- 面向切面编程(AOP):可以在不修改原有代码的基础上,对业务逻辑进行增强,如日志记录、事务管理等。
- 依赖注入实现方式:
- 构造器注入:通过构造函数传入依赖对象。例如:
- 核心特性:
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
- **setter 方法注入**:通过 setter 方法设置依赖对象。例如:
public class UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
- Spring Boot 与 Spring 相比的优势:
- 简化配置:Spring Boot 采用了自动配置的机制,大大减少了 XML 配置文件的编写,默认配置就能满足很多常见的应用场景,使开发者可以更专注于业务逻辑的实现。
- 内置依赖:Spring Boot 内置了很多常用的依赖,如 Tomcat、Spring Data、Spring Security 等,开发者不需要手动添加这些依赖,减少了配置和开发的工作量,能够快速搭建项目。
- MyBatis 工作原理及核心组件:
- 工作原理:MyBatis 先读取配置文件,通过 SqlSessionFactory 创建 SqlSession,SqlSession 是 MyBatis 执行数据库操作的主要接口。然后由 SqlSession 执行 SQL 语句,SQL 语句可以通过 XML 配置文件或注解定义。MyBatis 通过反射机制创建和操作 Java 对象与数据库表之间的映射关系。
- 核心组件:
- SqlSessionFactory:创建 SqlSession 的工厂,它负责读取 MyBatis 的配置文件并构建内部的各种组件。
- SqlSession:执行 SQL 语句的会话,通过它可以执行查询、插入、更新、删除等操作,并返回结果。
- Mapper 接口:定义了与数据库表操作对应的方法,通过 SqlSession 调用这些方法来执行具体的 SQL 语句。例如,定义一个 UserMapper 接口,其中包含查询用户、插入用户等方法,MyBatis 会根据接口定义的方法名和参数来动态生成 SQL 语句并执行。