面试官:请简要介绍一下 Java 核心知识在实际项目中的应用场景。
王铁牛:嗯,Java 核心知识比如面向对象编程,在项目里可以让代码结构更清晰,不同功能封装到不同类里,方便维护和扩展。
面试官:那说说 JUC 包下的常用类及其作用。
王铁牛:像 CountDownLatch 可以用来控制多个线程等待,等所有条件满足了再一起执行。
面试官:还不错。接下来讲讲 JVM 的内存模型。
王铁牛:这个嘛,就是有堆、栈、方法区这些,对象在堆里创建,方法调用在栈里执行,类信息啥的在方法区。
面试官:第一轮面试结束,稍等一下进入第二轮。
面试官:多线程编程中,如何避免死锁?
王铁牛:那就是要避免循环等待资源,给资源加锁的时候按顺序来。
面试官:线程池的核心参数有哪些,分别有什么作用?
王铁牛:有 corePoolSize、maximumPoolSize 等,corePoolSize 是核心线程数,maximumPoolSize 是最大线程数。
面试官:说说 HashMap 的底层实现原理。
王铁牛:它是数组加链表再加红黑树,通过 key 的哈希值找到对应的位置。
面试官:第二轮面试结束,准备第三轮。
面试官:Spring 框架中,依赖注入的方式有哪些?
王铁牛:有构造器注入、setter 注入这些。
面试官:Spring Boot 自动配置的原理是什么?
王铁牛:不太清楚,大概就是能自动配置一些常用的组件啥的吧。
面试官:MyBatis 的缓存机制是怎样的?
王铁牛:有一级缓存和二级缓存,一级缓存是 SqlSession 级别的,二级缓存是 namespace 级别的。
面试官:本次面试结束,回去等通知吧。
答案:
- Java 核心知识在实际项目中的应用场景:
- 面向对象编程:将项目中的各种功能封装到不同的类中,提高代码的可维护性和可扩展性。例如,一个电商项目中,可以将用户管理、商品管理、订单管理等功能分别封装到不同的类里。用户类负责处理用户的注册、登录等操作;商品类管理商品的信息、库存等;订单类处理订单的创建、支付等逻辑。这样不同功能模块清晰划分,修改和扩展某个功能时不会影响其他模块。
- 异常处理:在项目中捕获和处理各种异常情况,保证程序的稳定性。比如在文件读取操作中,使用 try - catch 块捕获可能出现的 FileNotFoundException 等异常,避免程序因为文件读取失败而崩溃,并可以根据不同的异常情况给出相应的提示信息。
- 多态性:可以根据对象的实际类型动态调用相应的方法。例如,在一个图形绘制项目中,定义一个 Shape 父类,Rectangle 和 Circle 等子类继承自 Shape。可以创建一个 Shape 类型的数组,将 Rectangle 和 Circle 对象放入数组中,通过循环调用数组中对象的 draw 方法,根据对象的实际类型(Rectangle 或 Circle)绘制不同的图形,实现代码的灵活性和可扩展性。
- JUC 包下的常用类及其作用:
- CountDownLatch:用于控制多个线程等待,直到某个条件满足后再一起执行。例如,在一个游戏场景中,有多个玩家角色,需要等待所有玩家都准备好(比如点击了“准备”按钮)后,游戏才能开始。可以使用 CountDownLatch,每个玩家准备好时调用 countDown()方法,主线程调用 await()方法等待,当计数器的值减为 0 时,游戏开始。
- CyclicBarrier:让一组线程互相等待,直到到达某个公共屏障点。比如在一个团队开发项目中,多个模块的开发人员完成各自的任务后,需要等待所有模块都完成集成测试才能进行正式发布。可以使用 CyclicBarrier,每个开发人员完成任务后调用 await()方法,当所有开发人员都调用 await()方法后,到达屏障点,进行集成测试。
- Semaphore:控制同时访问某个资源的线程数量。例如,在一个停车场管理系统中,停车场有固定数量的车位。可以使用 Semaphore 来控制进入停车场的车辆数量,当车位满时,其他车辆需要等待。每个车辆进入停车场时获取一个许可(调用 acquire()方法),离开停车场时释放许可(调用 release()方法)。
- JVM 的内存模型:
- 堆:是 JVM 中最大的一块内存区域,用于存储对象实例。所有的对象实例都在堆中创建。例如,在一个 Java 程序中创建了多个 User 对象,这些 User 对象都存储在堆中。堆又可以分为新生代、老年代和永久代(在 JDK8 及以后,永久代被元空间取代)。新生代主要用于存放新创建的对象,老年代存放经过多次垃圾回收后仍然存活的对象。
- 栈:每个线程都有自己独立的栈空间,用于存储局部变量、方法调用等信息。当一个方法被调用时,会在栈中创建一个栈帧,栈帧中包含局部变量表、操作数栈等。例如,在一个方法中定义了局部变量 int num = 10; 这个变量 num 就存储在该方法对应的栈帧的局部变量表中。当方法执行结束,栈帧被销毁,局部变量也随之消失。
- 方法区:存储类信息、常量、静态变量等。例如,类的字节码文件加载到 JVM 后,类的元数据(如类名、方法名、字段信息等)就存放在方法区。方法区中的数据可以被多个线程共享。在 JDK8 及以后,方法区的实现发生了变化,使用元空间来代替永久代,元空间的内存不再在 JVM 内部,而是使用本地内存。
- 多线程编程中避免死锁的方法:
- 避免循环等待资源:多个线程在获取资源时按照一定的顺序进行,避免形成循环等待。例如,有线程 T1、T2 和资源 R1、R2。如果 T1 先获取 R1,T2 先获取 R2,然后 T1 尝试获取 R2,T2 尝试获取 R1,就会形成死锁。所以规定 T1 和 T2 都先获取 R1,再获取 R2,这样就不会出现死锁。
- 使用定时锁:在获取锁时设置一个超时时间,如果在规定时间内无法获取到锁,就放弃获取,避免一直等待导致死锁。例如,使用 Lock 接口的 tryLock(long timeout, TimeUnit unit)方法,设置一个超时时间,当超过这个时间仍未获取到锁时,方法返回 false,线程可以进行其他操作,而不是一直阻塞。
- 避免锁嵌套:尽量减少锁的嵌套使用,避免在持有一个锁的情况下再去获取另一个锁。例如,在一个方法中,不要在已经持有锁 A 的情况下,又去获取锁 B,这样容易导致死锁。如果确实需要在持有一个锁的情况下获取另一个锁,可以先释放当前持有的锁,再获取另一个锁,操作完成后再重新获取原来的锁。
- 线程池的核心参数及其作用:
- corePoolSize:核心线程数。当提交的任务数小于 corePoolSize 时,线程池会创建新的线程来执行任务。例如,一个线程池的 corePoolSize 为 5,当提交 3 个任务时,会创建 3 个线程来执行这 3 个任务。
- maximumPoolSize:最大线程数。当提交的任务数大于 corePoolSize 且任务队列已满时,会创建新的线程,直到线程数达到 maximumPoolSize。如果此时线程数已经达到 maximumPoolSize,且任务队列也已满,那么后续提交的任务会根据拒绝策略进行处理。比如一个线程池的 maximumPoolSize 为 10,corePoolSize 为 5,任务队列容量为 3,当提交 8 个任务时,前 5 个任务由 corePoolSize 个线程执行,接下来 3 个任务放入任务队列,再提交任务时,会创建新的线程,直到线程数达到 10。
- keepAliveTime:线程池中的线程在空闲时的存活时间。当线程空闲时间超过 keepAliveTime 时,线程会被销毁,直到线程数不低于 corePoolSize。例如,keepAliveTime 设置为 60 秒,一个线程执行完任务后空闲了 70 秒,那么这个线程会被销毁。
- unit:keepAliveTime 的时间单位。可以是 TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。
- workQueue:任务队列,用于存放提交的任务。当提交的任务数大于 corePoolSize 时,任务会被放入任务队列中等待执行。常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。例如,使用 ArrayBlockingQueue 作为任务队列,它有固定的容量,当任务队列已满且线程数达到 maximumPoolSize 时,后续任务会根据拒绝策略处理;而 LinkedBlockingQueue 是无界队列,理论上可以存放无限个任务,当线程数达到 maximumPoolSize 时,新提交的任务会一直存放在任务队列中。
- threadFactory:线程工厂,用于创建线程。可以通过自定义线程工厂来设置线程的名称、优先级等属性。例如,创建一个自定义的线程工厂,给每个创建的线程设置一个有意义的名称,方便在日志中查看线程的执行情况。
- handler:拒绝策略,当线程数达到 maximumPoolSize 且任务队列已满时,会调用拒绝策略来处理新提交的任务。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用者线程执行任务)、DiscardPolicy(丢弃新提交的任务)、DiscardOldestPolicy(丢弃任务队列中最旧的任务)。
- HashMap 的底层实现原理:
- 数组 + 链表 + 红黑树:HashMap 底层是一个数组,数组的每个元素是一个链表节点(JDK8 之前)或链表节点与红黑树节点(JDK8 及以后)。当向 HashMap 中插入键值对时,首先通过 key 的哈希值计算出在数组中的索引位置。如果该位置为空,直接插入新的节点;如果该位置不为空,就会形成链表,新节点会插入到链表的头部(JDK8 之前)。当链表长度达到一定阈值(默认 8)时,链表会转换为红黑树,以提高查找效率。例如,有一系列键值对,它们的哈希值经过计算后都映射到数组的同一个位置,就会在这个位置形成链表。随着插入的键值对越来越多,链表长度超过 8 时,就会将链表转换为红黑树。红黑树是一种自平衡二叉查找树,它的查找、插入和删除操作的时间复杂度都是 O(log n),相比链表的 O(n) 大大提高了效率。在查找时,通过 key 的哈希值找到对应的数组位置,然后在链表或红黑树中进行查找。如果是链表,就依次遍历链表节点比较 key 是否相等;如果是红黑树,就按照红黑树的查找规则进行查找。
- Spring 框架中依赖注入的方式:
- 构造器注入:通过构造函数来注入依赖。例如,有一个 UserService 类依赖于 UserDao 类,在 UserService 的构造函数中传入 UserDao 对象。这样当创建 UserService 对象时,就会同时注入 UserDao 对象。代码示例:
public class UserService {
private UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
//...
}
- setter 注入:通过 setter 方法来注入依赖。还是以 UserService 和 UserDao 为例,在 UserService 中提供一个 setUserDao 方法,然后通过这个方法注入 UserDao 对象。代码示例:
public class UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
//...
}
- 基于注解的注入:使用 @Autowired 注解进行注入。可以在字段上使用 @Autowired,也可以在 setter 方法上使用 @Autowired。例如:
public class UserService {
@Autowired
private UserDao userDao;
//...
}
- Spring Boot 自动配置的原理:
- Spring Boot 自动配置是基于 Spring 的条件化配置实现的。它通过大量的 @Configuration 注解类和 @Conditional 注解来实现。当 Spring Boot 应用启动时,会扫描 classpath 下所有的 jar 包和配置类。这些配置类中定义了各种自动配置类,每个自动配置类都会根据特定的条件来决定是否生效。例如,当项目中引入了 Spring Data JPA 相关的依赖时,Spring Boot 会自动配置 JpaRepositoriesAutoConfiguration 类。这个类会根据一些条件,比如是否存在 JpaRepository 接口的实现类等,来决定是否创建 JPA 相关的组件,如 EntityManagerFactory、JpaTransactionManager 等。这些自动配置类还会导入一些默认的配置属性,开发者可以通过 application.properties 或 application.yml 文件来覆盖这些默认配置。同时,Spring Boot 还提供了一种机制,让开发者可以通过自定义配置类来进一步定制自动配置的行为。比如,开发者可以创建一个自己的配置类,继承自某个自动配置类,然后通过 @ConfigurationProperties 注解来绑定自定义的属性,从而改变自动配置的默认行为。
- MyBatis 的缓存机制:
- 一级缓存:是 SqlSession 级别的缓存。当执行 SQL 查询时,会先从一级缓存中查找,如果找到对应的结果,就直接返回,不会再执行 SQL 查询。例如,在同一个 SqlSession 中,多次执行相同的查询语句,第一次查询后,结果会被缓存到一级缓存中,后续再次执行相同查询时,直接从缓存中获取结果。一级缓存的生命周期是 SqlSession 的生命周期,当 SqlSession 关闭时,一级缓存也会被清空。
- 二级缓存:是 namespace 级别的缓存。不同的 SqlSession 可以共享二级缓存。开启二级缓存后,对于同一个 namespace 下的查询,当一个 SqlSession 执行查询并将结果放入二级缓存后,其他 SqlSession 执行相同查询时可以从二级缓存中获取结果。例如,在一个项目中,有多个业务模块可能会执行相同的关于某个实体类的查询,开启二级缓存后,这些查询可以共享缓存结果。二级缓存的实现需要在 MyBatis 的配置文件中进行相关配置,并且实体类需要实现 Serializable 接口。当数据发生变化时,需要手动清空二级缓存,以保证缓存数据的一致性。