问题来源于牛某,如下图所示:
![](/image/1687329809095-7704a777-e20c-42ae-aad4-532c2035e358.png)
答案解析
1.说一下Java内存模型?
注意,当问到 Java 内存模型的时候,不要和 JVM 内存布局(JVM 运行时数据区域)搞混了,这块问的不是 JVM 内存的布局是啥,而是 Java 内存模型,Java Memory Model,简称 JMM。
Java 内存模型是用来定义 Java 线程和内存之间的操作规范的,目的是解决多线程正确执行的问题。 Java 内存模型规范的定义确保了多线程程序的可见性、有序性和原子性,从而保证了线程之间正确的交互和数据一致性。 Java 内存模型主要包括以下内容:
- 主内存(Main Memory):所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。
- 工作内存(Working Memory):每个线程拥有自己的工作内存,用于存储主内存中的数据的副本。线程只能直接操作工作内存中的数据。
- 内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。
- 原子性(Atomicity):JMM 保证基本数据类型(如 int、long)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。
- 可见性(Visibility):JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。
- 有序性(Ordering):JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等。
Java 内存模型通过以上规则和语义,提供了一种统一的内存访问方式,使得多线程程序的行为可预测、可理解,并帮助开发者编写正确和高效的多线程代码。开发者可以利用 JMM 提供的同步机制(如关键字 volatile、synchronized、Lock 等)来实现线程之间的同步和通信,以确保线程安全和数据一致性。
内存模型的简单执行示例图如下:
![](/image/1687333041836-cc30f37e-c955-43ee-92e8-8b044b0ea650.png)
2.List、Set、Map的区别?
List、Set 和 Map 都是常见的集合接口,用于存储和操作数据,它们的区别如下:
- List 是有序的集合,允许重复元素,可以按索引访问。它的常见实现类有 ArrayList、LinkedList 和 Vector。
- Set 是无序的集合,不允许重复元素。它的常见实现类有 HashSet、TreeSet 和 LinkedHashSet。
- Map 是键值对的映射集合,键不允许重复,值可以重复。它的常见实现类有 HashMap、TreeMap 和 LinkedHashMap。
3.介绍一下设计模式?
当聊到设计模式时,可以举一些常见的设计模式,以及这些设计模式的具体应用,比如以下这些:
- 工厂模式(Factory Pattern): 工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,使得应用程序可以更加灵活和可维护。比如在 Spring 中,FactoryBean 就是一个工厂模式的实现,使用它的工厂模式就可以创建出来其他的 Bean 对象。
- 单例模式(Singleton Pattern):单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供了一个全局访问点。比如在 Spring 中,所以的 Bean 默认是单例的,这意味着每个 Bean 只会被创建一次,并且可以在整个应用程序中共享。
- 代理模式模式(Proxy Pattern): 代理模式是一种结构型设计模式,它允许开发人员在不修改原有代码的情况下,向应用程序中添加新的功能。比如在 Spring AOP(面向切面编程)就是使用代理模式的实现,它允许开发人员在方法调用前后执行一些自定义的操作,比如日志记录、性能监控等。
- 模板方法模式(Template Pattern):模板方法模式是最常用的设计模式之一,它是指定义一个操作算法的骨架,而将一些步骤的实现延迟到子类中去实现,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。此模式是基于继承的思想实现代码复用的。比如在 MyBatis 中的典型代表 BaseExecutor,在 MyBatis 中 BaseExecutor 实现了大部分SQL 执行的逻辑。
- 观察者模式(Observer Pattern):定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。比如事件驱动、消息传递等功能时,可以使用观察者模式,例如 Spring Event 事件机制。
- 适配器模式(Adapter Pattern):适配器模式是一种结构型设计模式,它允许开发人员将一个类的接口转换成另一个类的接口,以满足客户端的需求。在 Spring 中,适配器模式常用于将不同类型的对象转换成统一的接口,比如将 Servlet API 转换成 Spring MVC 的控制器接口。
4.MySQL存储结构?
在 MySQL 默认的存储引擎 InnoDB 中,所有数据都被逻辑地存放在一个空间内,称为表空间(tablespace),而表空间由段(sengment)、区(extent)、页(page)组成。
![](/image/1687334145394-c8d1bbe2-e6e8-4d35-8cdf-a81ef06e9507.png)
(图片来源:冰河的博客)
其中:
- 表空间(tablespace)是一组逻辑文件,用于存储表和索引数据。它是 InnoDB 存储引擎的基本组织单位,可以看作是一个独立的存储区域。
- 段(segment)是表空间的子单位,它是逻辑上连续的数据块,存储了一组数据页。段可以是用于存储表数据的数据段,也可以是用于存储索引的索引段。
- 区(extent)是段的子单位,它是一组连续的物理页,通常是 64 个连续的页。在一个区中,可以存储一个或多个数据页。
- 页(page)是 InnoDB 存储数据的最小单位,通常为 16KB 大小。每个页都有一个唯一的标识符,用于在磁盘和内存中定位和管理数据。InnoDB 使用页来存储表的行数据和索引数据。
表空间包含多个段,段包含多个区,区包含多个页。这种层次结构帮助 InnoDB 有效地管理存储空间,并提供高性能和可靠性的数据访问。
举一个例子,假设表空间就是整个书架,段是书架的每一层,区是每一层上的每一个格子,页是每个格子里面放的一页纸。这种层次结构让 MySQL InnoDB 能够更有效地管理和访问数据,就像你可以根据书的编号和位置快速找到你想要的书籍一样。
5.索引失效的场景?
常见的索引失效场景有以下这些:
- 未遵循最左匹配原则
- 使用列运算
- 使用函数方法
- 类型转换
- 使用 is not null
- 错误的模糊匹配,使用右 % 开始查询。
具体内容请参考:https://www.javacn.site/interview/mysql/indexinvalid.html
6.为什么使用函数索引会失效?
使用函数导致索引失效的原因,是因为函数会对索引列的值进行计算或转换,导致 MySQL 无法直接匹配索引中的数据,从而无法使用索引来进行查询优化了。
比如以下索引结构:
![](/image/1687335123416-f383e7df-3647-42ab-a623-d8824a2be16c.png)
如果你的 SQL 语句条件用的是 where t_modified='2018-7-1' 的话,引擎就会按照上面绿色箭头的路线,快速定位到 t_modified='2018-7-1' 需要的结果。实际上,B+ 树提供的这个快速定位能力,来源于同一层兄弟节点的有序性。
但是,如果计算 month() 函数的话,你会看到传入 7 的时候,在树的第一层就不知道该怎么办了。
也就是说,对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
7.Spring事务有哪两种?
Spring 提供了两种事务:
- 编程式事务
- 声明式事务
它们的实现如下。
7.1 编程式事务
@RestController
public class UserController {
// 事务管理器
@Resource
private DataSourceTransactionManager dataSourceTransactionManager;
// 定义事务属性
@Resource
private TransactionDefinition transactionDefinition;
@Resource
private UserService userService;
@RequestMapping("/sava")
public Object save(User user) {
// 开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager
.getTransaction(transactionDefinition);
// 插入数据库
int result = userService.save(user);
// 提交事务
dataSourceTransactionManager.commit(transactionStatus);
// // 回滚事务
// dataSourceTransactionManager.rollback(transactionStatus);
return result;
}
}
7.2 声明式事务
声明式事务的实现非常简单,只需要给类或方法上添加 @Transactional 注解即可,如下代码所示:
@RequestMapping("/save")
@Transactional // 添加此注解就是声明式事务
public Object save(User user) {
int result = userService.save(user);
return result;
}
8.@Transactional实现原理?
@Transactional 注解的实现原理是基于 Spring AOP,Spring AOP 又是基于动态代理(模式)的实现。
在 Spring 中,@Transactional 注解会通过 AOP 机制生成一个代理 connection 对象,并将其放入 DataSource 实例的某个与 DataSourceTransactionManager 相关的某处容器中。这样,当开始执行目标方法之前先开启事务,如果方法正常执行完成则提交事务,如果执行中出现了异常,则会回滚事务。
9.事务如何合并@Transactional修饰的方法?
当一个被 @Transactional 修饰的方法调用另一个被 @Transactional 修饰的方法时,事务会通过事务的传播机制,也就是 Propagation 属性的设置来合并事务,事务传播机制总共有以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
所以,当你不设置 @Transactional 的 Propagation 属性时也会合并事务。
而事务的传播机制 REQUIRED 合并事务是通过事务同步器(TransactionSynchronization)和事务管理器(TransactionManager)来完成的。
- 事务同步器(TransactionSynchronization):事务同步器是一个接口,定义了在事务生命周期中的各个阶段执行的回调方法。它包括事务开始前、事务提交/回滚后以及事务完成后等方法。事务同步器允许注册和执行事务相关的操作,如预提交、后提交、回滚等。
- 事务管理器(TransactionManager):事务管理器是 Spring 框架中用于管理事务的核心接口。它负责控制事务的开始、提交或回滚,并提供事务的隔离级别和超时等属性的管理。事务管理器与底层的数据访问技术(如JDBC、Hibernate、JPA等)进行交互,确保数据操作与事务的一致性。
基于以上概念,REQUIRED 事务传播机制的实现步骤如下:
- 当一个带有 @Transactional(propagation = Propagation.REQUIRED) 注解的方法被调用时,Spring 会检查当前是否存在一个活动的事务。
- 如果当前存在一个事务,被调用方法会加入该事务,并与调用方法共享同一个事务。Spring 通过事务同步器(TransactionSynchronization)将被调用方法注册为事务的一部分。
- 如果当前不存在事务,事务管理器(TransactionManager)会创建一个新的事务,并将其与被调用方法关联起来。被调用方法成为这个新事务的第一个参与者。
- 在事务执行过程中,如果被调用方法抛出异常,则整个事务将被标记为回滚,并触发事务回滚操作。
- 在事务执行完成后,事务管理器负责提交整个事务,并触发事务提交操作。
- 在事务提交或回滚后,事务同步器会调用事务完成的回调方法,执行相应的清理工作。
通过以上步骤,REQUIRED 事务传播机制实现了被调用方法与调用方法共享或创建新的事务,并保证在事务执行过程中的一致性和隔离性。
10.Redis数据类型有哪些?
Redis 常用的数据类型有 5 种:String 字符串类型、List 列表类型、Hash 哈希表类型、Set 集合类型、Sorted Set 有序集合类型,如下图所示: 这 5 种常用类型的用途如下:
- String:字符串类型,常见使用场景是:存储 Session 信息、存储缓存信息(如详情页的缓存)、存储整数信息,可使用 incr 实现整数+1,和使用 decr 实现整数 -1;
- List:列表类型,常见使用场景是:实现简单的消息队列、存储某项列表数据;
- Hash:哈希表类型,常见使用场景是:存储 Session 信息、存储商品的购物车,购物车非常适合用哈希字典表示,使用人员唯一编号作为字典的 key,value 值可以存储商品的 id 和数量等信息、存储详情页信息;
- Set:集合类型,是一个无序并唯一的键值集合,它的常见使用场景是:关注功能,比如关注我的人和我关注的人,使用集合存储,可以保证人员不会重复;
- Sorted Set:有序集合类型,相比于 Set 集合类型多了一个排序属性 score(分值),它的常见使用场景是:可以用来存储排名信息、关注列表功能,这样就可以根据关注实现排序展示了。
更多详情请参考:https://www.javacn.site/interview/redis/types.html
11.Redis如何实现分布式锁?
Redis 作为一个独立的三方系统(通常被作为缓存中间件使用),其天生的优势就是可以作为一个分布式系统(分布式锁)来使用,如下图所示:
![](/image/1687336639537-779f9d25-af98-49c9-be8f-ba7eb0f09687.png)
在 Redis 中实现分布式锁可以使用 SETNX 和 EXPIRE 命令来实现,SETNX 是 "SET if Not eXists" 的缩写,是一个原子性操作,用于在指定的 key 不存在时设置 key 的值。如果 key 已经存在,SETNX 操作将不做任何事情,返回失败;如果 key 不存在,SETNX 操作会设置 key 的值,并返回成功。而 EXPIRE 是设置锁的过期时间的,主要为了防止死锁的发生,SETNX + EXPIRE 的实现命令如下:
![](/image/1687336644743-b3914294-1d59-4b79-8639-3e6437605a6b.png)
其中“nx”表示 not exists 不存在则设置 key,“ex 10”表示过期时间为 10 秒,“mylock”值为 key,“lock”值为 value。 更多详情请参考:https://www.javacn.site/interview/redis/redis_lock.html
12.什么时候用RocketMQ?
RocketMQ 是一个分布式消息中间件系统,主要用于解决高吞吐量、低延迟的消息传递需求。以下是一些 RocketMQ 的使用场景:
- 异步消息传递:RocketMQ 提供了可靠的异步消息传递机制,可以在分布式系统中实现解耦和异步处理。例如,在电商平台中,当用户下单成功后,可以使用 RocketMQ 异步地发送订单信息给库存系统和支付系统,以提高系统的响应速度和可靠性。
- 流式数据处理:RocketMQ 支持高吞吐量的消息传递,适用于大规模的实时数据处理场景。例如,日志收集和分析系统可以使用 RocketMQ 作为消息中间件,将分布式系统产生的日志实时传递给日志处理系统,进行实时监控和分析。
- 事件驱动架构:RocketMQ 可以作为事件驱动架构的基础设施,实现系统间的事件通知和响应。例如,微服务架构中的各个服务可以通过 RocketMQ 发送事件消息来通知其他服务进行相应的操作,实现解耦和灵活的系统架构。
- 分布式事务消息:RocketMQ 提供了事务消息的支持,可以确保分布式系统中的消息发送和业务操作的原子性。在分布式事务场景下,可以使用 RocketMQ 发送事务消息,以确保消息的可靠传递和业务操作的一致性。
- 流量削峰与消息堆积:RocketMQ 支持消息的异步发送和批量发送,可以用于平滑处理系统的流量峰值和消息的堆积情况。例如,在双11等促销活动中,可以使用 RocketMQ 来处理大量的订单请求,避免系统过载和消息丢失。
所以,RocketMQ 在高吞吐量、低延迟、可靠性和扩展性等方面具备优势,适用于许多分布式系统和大规模数据处理场景。
13.说下RocketMQ和OpenFeign的应用场景?
RocketMQ 应用场景上个问题已经说了。
OpenFeign 的全称是 Spring Cloud OpenFeign,它是 Spring 官方推出的一种声明式服务调用和负载均衡组件。它的出现就是为了替代已经进入停更维护状态的 Feign(Netflix Feign)的。也就是说 OpenFeign(Spring Cloud OpenFeign)是 Feign 的升级版,因为 Feign 停更维护了,所以 Spring 官方需要推出了一个新的新的框架来对 Feign 功能进行升级和扩展。
OpenFeign 应用场景:
- 微服务架构:OpenFeign 可以在微服务架构中作为服务之间进行通信的客户端工具。它通过定义接口和注解的方式,使得服务之间的通信变得简单明了。开发人员只需要定义接口,而无需关注底层的 HTTP 请求和响应处理,从而提高开发效率。
- 客户端负载均衡:OpenFeign 集成了负载均衡功能,可以与负载均衡器(如 Ribbon)一起使用,实现客户端负载均衡。通过配置合适的负载均衡策略,可以将请求分发到多个服务实例上,提高系统的可用性和吞吐量。
- 服务降级与容错:OpenFeign 可以集成断路器(如 Hystrix)来实现服务的降级和容错处理。当服务不可用或出现异常时,可以定义降级策略,返回预设的响应,以防止故障在整个系统中扩散。
OpenFeign 具体使用可参考:https://juejin.cn/post/7101545210315800612
14.抽象类和接口的区别?
在 Java 中,抽象类和接口是两种不同的类类型。它们都不能直接实例化,并且它们都是用来定义一些基本的属性和方法的,但它们有以下几点不同:
- 定义不同:定义的关键字不同,抽象类是 abstract,而接口是 interface。
- 方法实现:抽象类可以包含抽象方法和具体方法,而接口只能包含方法声明(抽象方法)。
- 方法访问控制符不同:抽象类无限制,只是抽象类中的抽象方法不能被 private 修饰;而接口有限制,接口默认的是 public 控制符。
- 实现/继承数量不同:一个类只能继承一个抽象类,但可以实现多个接口。
- 包含变量不同:抽象类可以包含实例变量和静态变量,而接口只能包含常量。
- 构造函数不同:抽象类可以有构造函数,而接口不能有构造函数。
更多内容请参考:https://www.javacn.site/interview/basic/abstract-interface.html
15.什么时候用抽象类?什么时候用接口?
接口是用来定义规范的,而抽象类提供了代码重用。所以,当你要定义规范时可以使用接口,当你要重用代码时可以使用抽象类。
16.try、catch、finally各有return,会怎么执行?
程序最终会返回 finally 中的 return 值。
因为在 finally 块中使用了 return 语句,它将会覆盖在之前的 try 块或 catch 块中的返回值。
17.服务宕机时直接降级是最优解吗?
服务降级以及服务熔断和服务限流都不是最优解,因为以上所有操作都是以牺牲用户体验为前提的,而最优的解决方案,应该是让用户不受影响,让用户无感知的使用程序才对。
所以我们可以使用负载均衡与集群化,将服务部署在多台服务器上,通过负载均衡来分发请求,当其中一台服务宕机时,负载均衡可以自动将请求转发给其他正常运行的服务,通过集群化部署,可以提高系统的可靠性和可用性,让用户无感知才是最优解。
参考 & 鸣谢
冰河的博客
https://blog.csdn.net/MariaOzawa/article/details/107363136
小结
众安保险问的问题比较杂,有基础知识也有微服务的知识,应该是应聘者简历中有写到 Spring Cloud 相关的技术栈了,但相对来说基础的知识问的比较深,微服务则是点到为止。
以上内容来自我的 《Java 面试突击训练营》,这门课程是 有着 14 年工作经验(前 360 开发工程师),9 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课。
整个课程从 Java 基础到微服务 Spring Cloud、从实际开发问题到场景题应有尽有,如下图所示:
![](/image/camp.png)
全程通过视频直播 + 录播的方式,把 Java 常见的面试题系统的过一遍,遇到一个问题,把这个问题相关的内容都给大家讲明白,并且视频支持永久更新和观看。
上完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,想要了解详情,加我微信:vipStone【备注:训练营】
![](/image/weixin-2.png)