面试官:第一轮面试开始。首先问你,在多线程环境下,如何保证线程安全?
王铁牛:可以使用 synchronized 关键字呀,它能保证同一时刻只有一个线程访问被修饰的代码块或方法。
面试官:不错,回答正确。那再问你,线程池有哪些参数,分别有什么作用?
王铁牛:线程池有 corePoolSize、maximumPoolSize、keepAliveTime、unit 和 workQueue 这些参数。corePoolSize 是核心线程数,maximumPoolSize 是最大线程数,keepAliveTime 是线程池线程数超过 corePoolSize 时,多余线程在终止前等待新任务的最长时间,unit 是 keepAliveTime 的时间单位,workQueue 是任务队列。
面试官:嗯,回答得挺清晰。最后一个问题,简述一下 HashMap 的底层数据结构。
王铁牛:HashMap 底层是数组 + 链表 + 红黑树。当链表长度超过一定阈值时,链表会转换为红黑树,以提高查询效率。
面试官:好,第一轮面试结束。接下来进入第二轮面试。说说 JVM 的内存模型,都有哪些区域?
王铁牛:JVM 内存模型有堆、栈、方法区、程序计数器、本地方法栈。堆是存放对象实例的地方,栈存放局部变量和方法调用栈帧,方法区存储类信息、常量、静态变量等,程序计数器记录当前线程执行的字节码指令地址,本地方法栈用于执行本地方法。
面试官:还行。那 Spring 框架的核心特性有哪些?
王铁牛:Spring 框架的核心特性有依赖注入、面向切面编程、IoC 容器、事务管理等。
面试官:回答得马马虎虎。再问你,Spring Boot 自动配置原理是什么?
王铁牛:Spring Boot 自动配置原理就是通过 @EnableAutoConfiguration 注解,它会根据类路径下的依赖自动配置 Spring 应用上下文。
面试官:第二轮面试结束。现在开始第三轮面试。MyBatis 的缓存机制是怎样的?
王铁牛:MyBatis 有一级缓存和二级缓存。一级缓存是 SqlSession 级别的,在同一个 SqlSession 中查询数据会优先从一级缓存中获取。二级缓存是基于 namespace 级别的,多个 SqlSession 可以共享二级缓存。
面试官:最后问你两个问题。Dubbo 的集群容错策略有哪些?
王铁牛:Dubbo 的集群容错策略有 failover、failfast、failsafe、failback、forking 等。
面试官:还有,Redis 有哪些数据结构,分别适用于什么场景?
王铁牛:Redis 有字符串、哈希、列表、集合、有序集合这些数据结构。字符串适用于缓存、分布式锁等;哈希适用于存储对象;列表适用于消息队列、任务队列等;集合适用于去重、交集等操作;有序集合适用于排行榜等场景。
面试官:好了,三轮面试结束。回去等通知吧。
答案:
- 多线程环境下保证线程安全的方法:
- 使用 synchronized 关键字:它能保证同一时刻只有一个线程访问被修饰的代码块或方法。当一个线程访问被 synchronized 修饰的资源时,其他线程必须等待该线程释放锁后才能访问。例如:
在这个例子中,increment 方法被 synchronized 修饰,所以同一时刻只能有一个线程调用该方法,从而保证了 count 的线程安全。public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } } - 线程池的参数及作用:
- corePoolSize:核心线程数。当提交的任务数小于 corePoolSize 时,线程池会创建新的线程来执行任务。
- maximumPoolSize:最大线程数。当提交的任务数大于 corePoolSize 且任务队列已满时,线程池会创建新的线程,直到线程数达到 maximumPoolSize。
- keepAliveTime:线程池线程数超过 corePoolSize 时,多余线程在终止前等待新任务的最长时间。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列。用于存放提交到线程池但尚未被执行的任务。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。例如:
ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // corePoolSize 4, // maximumPoolSize 10, // keepAliveTime TimeUnit.SECONDS, new LinkedBlockingQueue<>(10) // workQueue ); - HashMap 的底层数据结构:
- HashMap 底层是数组 + 链表 + 红黑树。
- 初始时,HashMap 创建一个长度为 16 的数组。当添加键值对时,会通过 key 的 hash 值计算出在数组中的索引位置。如果该位置为空,则直接插入新节点。如果该位置已经有节点,则形成链表。当链表长度超过一定阈值(默认 8)时,链表会转换为红黑树,以提高查询效率。当红黑树节点数小于等于 6 时,又会转换回链表。例如:
HashMap<String, Integer> map = new HashMap<>(); map.put("key1", 1); map.put("key2", 2); // 随着不断添加元素,可能会发生链表到红黑树的转换 - JVM 的内存模型及区域:
- 堆:存放对象实例的地方。是 JVM 中最大的一块内存区域,被所有线程共享。
- 栈:存放局部变量和方法调用栈帧。每个线程都有自己独立的栈空间。
- 方法区:存储类信息、常量、静态变量等。被所有线程共享。
- 程序计数器:记录当前线程执行的字节码指令地址。是线程私有的。
- 本地方法栈:用于执行本地方法。也是线程私有的。例如:
public class JVMExample { public void method() { int a = 10; // a 存放在栈中 MyClass obj = new MyClass(); // obj 引用存放在栈中,对象实例存放在堆中 System.out.println(obj.field); // 从堆中获取对象的 field 属性,field 定义在方法区的类信息中 } } - Spring 框架的核心特性:
- 依赖注入:通过控制反转(IoC),将对象的创建和依赖关系管理交给 Spring 容器,由容器将依赖的对象注入到需要的地方。例如:
public class UserService { private UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } } // 在 Spring 配置文件中 <bean id="userDao" class="com.example.UserDaoImpl"/> <bean id="userService" class="com.example.UserService"> <constructor-arg ref="userDao"/> </bean>- 面向切面编程(AOP):通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。可以在不修改原有代码的情况下,对业务逻辑进行增强。例如:
// 定义切面 @Aspect public class LogAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("方法执行前:" + joinPoint.getSignature().getName()); } }- IoC 容器:负责创建、配置和管理对象之间的依赖关系。
- 事务管理:可以方便地实现事务的控制,确保数据的一致性。例如:
@Service @Transactional public class AccountService { public void transfer(String from, String to, double amount) { // 业务逻辑 } } - Spring Boot 自动配置原理:
- Spring Boot 自动配置是通过 @EnableAutoConfiguration 注解实现的。
- 它会根据类路径下的依赖自动配置 Spring 应用上下文。例如,当类路径下存在 spring-boot-starter-web 依赖时,会自动配置 Tomcat、Spring MVC 等相关组件。Spring Boot 会通过条件注解(如 @Conditional 系列注解)来判断是否需要进行某些配置。如果满足特定条件,就会自动创建相应的 bean 并进行配置。例如:
只有当类路径下存在 WebMvcConfigurer 类时,才会进行 WebMvc 相关的自动配置。@Configuration @ConditionalOnClass(WebMvcConfigurer.class) public class WebMvcAutoConfiguration { // 配置逻辑 } - MyBatis 的缓存机制:
- 一级缓存:是 SqlSession 级别的。在同一个 SqlSession 中查询数据会优先从一级缓存中获取。当执行 insert、update、delete 操作时,会清空一级缓存。例如:
SqlSession session = sqlSessionFactory.openSession(); User user1 = session.selectOne("selectUserById", 1); User user2 = session.selectOne("selectUserById", 1); // 从一级缓存中获取 session.update("updateUser", user); User user3 = session.selectOne("selectUserById", 1); // 一级缓存已清空,重新查询- 二级缓存:是基于 namespace 级别的,多个 SqlSession 可以共享二级缓存。需要在 MyBatis 配置文件中开启二级缓存,并在 Mapper 接口对应的 XML 文件中配置 标签。例如:
<mapper namespace="com.example.UserMapper"> <cache /> <select id="selectUserById" parameterType="int" resultType="User"> SELECT * FROM user WHERE id = #{id} </select> </mapper> - Dubbo 的集群容错策略:
- failover:失败自动切换。当调用失败时,会自动重试其他服务器,默认重试次数为 2 次。适用于读操作等对数据一致性要求不高的场景。
- failfast:快速失败。当调用失败时,立即抛出异常,不进行重试。适用于幂等操作。
- failsafe:失败安全。当调用失败时,直接忽略,不抛出异常。适用于写操作等对数据一致性要求不高的场景。
- failback:失败自动恢复。当调用失败时,会记录失败请求,然后定时重试。适用于消息通知等场景。
- forking:并行调用多个服务器,只要有一个成功就返回。适用于对实时性要求较高的场景。例如:
这里配置了 failover 集群容错策略,并且 methodName 方法的重试次数为 3 次。<dubbo:reference interface="com.example.Service" cluster="failover"> <dubbo:method name="methodName" retries="3"/> </dubbo:reference> - Redis 的数据结构及适用场景:
- 字符串:适用于缓存、分布式锁等。例如,缓存用户信息:
Jedis jedis = new Jedis("localhost"); jedis.set("user:1", "{\"name\":\"张三\",\"age\":20}"); String user = jedis.get("user:1");- 哈希:适用于存储对象。例如,存储用户对象:
jedis.hset("user:1", "name", "张三"); jedis.hset("user:1", "age", "20"); Map<String, String> userMap = jedis.hgetAll("user:1");- 列表:适用于消息队列、任务队列等。例如,模拟任务队列:
jedis.rpush("taskQueue", "task1", "task2"); String task = jedis.lpop("taskQueue");- 集合:适用于去重、交集等操作。例如,判断用户是否已存在:
jedis.sadd("users", "user1", "user2"); boolean exists = jedis.sismember("users", "user1");- 有序集合:适用于排行榜等场景。例如,存储用户积分排行榜:
jedis.zadd("scoreBoard", 100, "user1"); jedis.zadd("scoreBoard", 200, "user2"); Set<String> topUsers = jedis.zrevrange("scoreBoard", 0, 1);