9.数字马力面试
9.1 Java基础
9.1.1 volatile的概述和原理
在Java中volatile是一个防止指令重排以及保证可见性的关键字。
如果我们将变量声明为volatile,那么就指示JVM这个变量共享且不稳定,每次从主存中进行读取。AQS的state就是使用volatile修饰的。
借用Guide哥的图片:
在上图中,
- 主内存是多个线程共享的内存空间,主内存中存在一个多个共享变量;
- 每个线程都有自己的一个私有内存即本地内存,本地内存中存储该线程读取或写入主内存的共享变量的副本。线程在执行过程中操作的是副本,而不是直接操作主内存中的数据。
(1)可见性
可见性:当某个变量被volatile修饰时,该变量的修改对其他线程时立即可见的(其它线程立即能看到新值)。
实现机制:
- 内存屏障:JVM在读写volatile修饰的变量时,会在其前后插入内存屏障,保证volatile变量的读写操作不会被随意重新排序,并强制对变量的修改直接写入主内存。读取数据也是从主内存中读取,而非缓存或寄存器中的副本。
- 缓存一致性协议(MESI):多核系统中,每一个核都有自己的高速缓存。当一个线程修改volatile变量时,它会通过MESI通知其他核,促使他们更新自己的缓存或从主内存中读取数据,确保所有线程看到一致的变量。
(2)指令重排
指令重排:编译器和处理器通过改变代码执行顺序而不改变程序最终结果的一种优化手段。
编译器层面:编译器在生成字节码时,对volatile变量的访问插入相应的内存屏障
处理器层面:遵循内存屏障约束,不会对跨内存屏障的进行重排序。
JMM采取保守策略,进行内存屏障插入:
- 在每个volatile写操作前,插入StoreStore内存屏障;
- 在每个volatile写操作后,插入StoreLoad内存屏障;
- 在每个volatile读操作后,插入LoadLoad内存屏障;
- 在每个volatile读操作后,插入LoadStore内存屏障;
volatile写:
StoreStore屏障保证在volatile写之前,其前面的所有普通读写操作对任意处理器可见(即刷新到主内存)。
为什么volatile写之后加storeLoad屏障?
避免volatile写与后面可能的volatile读/写重排序。因为编译器无法判断一个volatile写之后是否需要插入一个StoreLoad内存屏障(如volatile后面立即return)。
为了保证正确实现volatile的内存语义,JMM采取保守策略:每个volatile写之后以及每个volatile读之前插入StoreLoad屏障。
volatile读:
LoadLoad屏障用来禁止处理器把上面的volatile与下面普通读重排序
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
9.1.2 HashMap的get方法和put方法源码
(1)put()方法
- 首先根据key计算hash值
- 使用hash值通过哈希函数(将hash值与数组长度-1进行位与运算)找到要插入或替换的数组索引
- 如果该位置没有元素则直接将键值对存储在该位置
- 如果该位置存在元素,则根据键的equals()方法判断键是否相等,如果相等则替换键的值,不相等则发生冲突,需要解决冲突;
- 解决哈希冲突的方法:使用链表法或红黑树等数据结构在该索引位置存储多个键值对,根据键的equals()方法找到对应的键值对进行替换
- 如果链表长度达到阈值(8),会先判断数组长度是否达到阈值(64),如果是则转换为红黑树,以提高查询效率;
- 如果哈希表容量不足,达到负载因子上线(默认0.75),则触发扩容操作,将原有的键值对重新分配到新的哈希表中。
(2)get()方法
- 首先计算key的hash值
- 使用hash值根据哈希函数(将hash值与数组长度-1进行位与运算)找到要查询的数组位置即索引;
- 如果该位置上不存在该元素则返回null;
- 如果该位置上存在元素,则使用equals()方法在链表或红黑树中查找对应的键值对
- 如果找到则返回,找不到则返回null
9.1.3 如何知晓多个线程执行完毕?(CountDowLatch)
9.1.4 AQS实现原理
9.2 分布式
9.2.1 如何实现幂等性?(不同用户同一手机号如何做幂等)
在分布式系统中,处理不同用户使用同一手机号确保幂等性时,主要挑战在于如何区分和处理这些请求,尤其当手机号作为关键标识。
-
用户身份验证与关联:
- 即使多个用户注册了相同的手机号,系统应当有能力通过其他身份验证信息(如用户名、密码、邮箱、验证码等)来区分这些用户。
- 确保在执行任何操作前,用户已经过认证,并且请求与用户的实际身份关联起来。
-
操作上下文明确化:
- 在请求中包含足够的上下文信息,比如用户ID、操作类型、时间戳等,使得系统能够识别这是来自不同用户的操作,而不是简单地基于手机号做判断。
-
使用唯一标识符:
- 为每个操作生成唯一的事务ID(Transaction ID)或请求ID,即使是同样的操作由不同用户发起,也拥有不同的ID,以此作为幂等处理的关键依据。
-
业务逻辑层的幂等设计:
- 在业务逻辑层面,对使用同一手机号的用户执行相同操作时,可以通过检查是否存在未完成的相同操作、或者检查该手机号与操作类型组合下的最新状态,来决定是否需要执行新的操作。
-
数据库层面的幂等控制:
- 利用数据库的事务机制、乐观锁或悲观锁来处理并发请求。例如,如果两个用户几乎同时尝试绑定同一个手机号,数据库可以通过版本控制来确保只有一个操作成功。
-
防重提交机制:
- 实施防重提交机制,记录每个请求的状态,对于已经处理过的请求(不论成功还是失败),后续的重复请求直接根据之前的处理结果响应,避免重复执行。
-
事件溯源与状态机:
- 对于复杂业务流程,采用事件溯源(Event Sourcing)记录每一个状态变更的事件,结合状态机模型确保状态迁移的正确性,从而达到幂等处理的目的。
9.3 Spring
9.3.1 Spring中FactoryBean与BeanFactory的区别
在Spring中BeanFactory用于生产和管理Bean,可以通过其getBean()方法根据Bean的名字获取Bean。
FactoryBean是通过getObject()返回实例,如果没有使用&符号修饰BeanName时返回的是实现了FactoryBean接口实现类中的自定义对象所在的类;如果通过&修饰BeanName,那么返回的是FactoryBean接口的实现类。他为Bean提供了更灵活的配置。
9.3.2 Spring中的BeanDifition
(1)概述
BeanDefition是Spring中一个核心概念,主要用于描述一个Bean的定义信息。当Spring容器初始化或加载时,它会读取配置元数据(XML配置、注解、Java配置类),并根据这些信息创建BeanDefition对象。每个BeanDefition对象都包含如何创建一个Bean实例的所有必要信息,如Bean的类名、属性、作用域、初始化方法、销毁方法、依赖关系等。
(2)作用
- 配置解析:将配置元数据解析为结构化的内部表示形式
- 管理Bean元数据:存储Bean的的配置信息(类名、作用域、初始方法和销毁方法、依赖注入(构造、setter、Bean引用)、自动装配(byName、ByType)、懒初始化、代理设置、初始化配置或设置属性、别名、优先级、依赖的Bean、生命周期回调方法(除初始化和销毁外,有@PostConstruct、@PreDestroy)、异步方法(@Async))
- 控制Bean的生命周期:使Spring能够管理Bean的的创建、初始化、使用和销毁;
- 依赖注入:描述Bean的之间的依赖关系,使Spring容器在实例化Bean时自动解决这些问题。
- 灵活性和扩展性:允许运行时修改Bean的的定义,提升高度的灵活性和配置性
(3)原理
- 解析与注册:Spring容器通过不同的BeanDefitionReader读取配置信息(XML、注解、配置类)并将其转换为BeanDefition对象。这些对象注入到BeanFactory中(或更具体实现类,如ApplicationContext中)。
- 属性解析:BeanDefition中可能包含其他属性值、其他Bean的引用,Spring会解析这些引用,并处理占位符、环境变量等。
- 依赖检查与解析:实例化前检查一俩关系,并根据依赖的BeanDefition创建或获取相应的Bean实例
- 实例化和初始化:Spring根据BeanDefition信息创建Bean实例,并调用初始化方法
(4)相对于反射的作用
BeanDefition不能直接用于反射,但是其包含的信息对于反射至关重要。Spring容器是根据BeanDefition存储的类名(beanClassName)和其他配置信息执行反射操作。
- 类名信息:每个BeanDefition对象包含每个Bean的全限定类名,Spring利用该类名通过Java反射的API(如Class.forName()方法)加载对应的类;
- 属性注入和初始化方法:如果BeanDefinition中包含属性或初始化方法的信息,可以通过反射调用来设置这些属性和执行初始化逻辑
9.3.3 Spring中用到的设计模式
(1)工厂模式
如Spring的工厂通过BeanFactory和ApplicationContext创建Bean。
- BeanFactory:使用时才注入,占更少内存,程序启动速度更快。
- ApplicationContext:不管是否用到,一次性创建所有Bean的,实现BeanFactory接口。
(2)单例模式
Spring中的Bean默认是单例模式,通过ConcurrentHashMap通过单例注册表方式。
(3)动态代理模式
- 如果代理对象实现了接口,那么采用JDK动态代理,如果没有实现接口,则采用CGlib(基于字节码)。
- Spring后期引入了AspectJ AOP(基于字节码),相对于Spring AOP功能更强,如果切面较少使用SpringAOP,如果切面较多使用AspectJ AOP。
(4)模板方法设计模式
该设计模式是一种行为设计模式,它定义了一些算法的骨架,将一些步骤延迟到子类中。即重定义该方法。
如Spring中的JdbcTemplate类。
(5)观察者模式
观察者模式是一种对象行为模式,表示对象与对象之间具有依赖关系。当一个对象作出改变时,这个对象依赖的对象也会作出改变。
Spring的事件驱动模型就是观察者模式经典的应用。
- 事件角色:ApplicationEvent(org.springframework.context包下)充当事件的角色,这是一个抽象类。
- 事件监听者角色:ApplicationListener充当了事件监听者的角色,它是一个接口,里面只定义了一个onApplicationEvent()方法来处理ApplicationEvent。
- 事件发布者角色:ApplicationEventPublisher充当了事件的发布者,它也是个接口。
Spring事件流程:
- 定义一个事件:继承ApplicationEvent类,并写相应的构造;
- 定义事件监听器:实现ApplcaiitionListener接口,重写onApplcaitionEvent()方法。
- 使用事件发布者发布事件:可以通过ApplcaitionEventPublisher的publishEvent()方法发布事件。
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数 public class DemoEvent extends ApplicationEvent{ private static final long serialVersionUID = 1L; private String message; public DemoEvent(Object source,String message){ super(source); this.message = message; } public String getMessage() { return message; } // 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法; @Component public class DemoListener implements ApplicationListener<DemoEvent>{ //使用onApplicationEvent接收消息 @Override public void onApplicationEvent(DemoEvent event) { String msg = event.getMessage(); System.out.println("接收到的信息是:"+msg); } } // 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。 @Component public class DemoPublisher { @Autowired ApplicationContext applicationContext; public void publish(String message){ //发布事件 applicationContext.publishEvent(new DemoEvent(this, message)); } }
(6)装饰器模式
装饰器模式可以动态地给对象增加额外属性或行为。相对于继承,该模式更加灵活。Spring配置DataSource时,DataSource可能是不同数据库和数据源。我们需要根据客户的需求在少修改原有代码下切换不同的数据源?这个时候就可用到装饰者模式。
(7)策略模式
Spring的资源访问接口是基于策略模式实现的。Spring框架大量使用了Resource接口访问底层资源。Resource接口本身没有提供访问任何底层资源的实现逻辑,针对不同的底层资源,Spring将会提供不同的Resource实现类,不同的实现类负责不同的资源访问类型。
- Spring 为 Resource 接口提供了如下实现类:
- UrlResource:访问网络资源的实现类。
- ClassPathResource:访问类加载路径里资源的实现类。
- FileSystemResource:访问文件系统里资源的实现类。
- ServletContextResource:访问相对于 ServletContext 路径里的资源的实现类.
- InputStreamResource:访问输入流资源的实现类。
- ByteArrayResource:访问字节数组资源的实现类。
9.4 RocketMQ
9.4.1 RocketMQ中的消息存在什么log中?(CommitLog)
在Apache RocketMQ中,消息主要存储在以下几种类型的日志文件中:
- CommitLog:RocketMQ消息存储的核心部分,所有主题(topic)的消息实体内容都被顺序写入该文件。CommitLog保证了消息的有序性和持久性,每个消息在CommitLog中都有一个全局唯一的物理偏移量(offset)。
- ConsumerQueue(消息队列):为提高消息查询效率,RocketMQ为每个Topic的每个消息队列创建了一个ConsumerQueue文件。它存储消息在CommitLog中的偏移量,消息长度以及tag(消息分类的标签)的hashcode等信息,但不存储消息的实际内容。消费者通过ConsumerQueue文件定位消息在CommitLog中的位置进行消费。
- IndexFile(索引文件):用于加快消息的查询速度,特别是按照消息Tag或时间跨度查询时。IndexFile存储了消息的索引信息,包括消息的offset、消息的tag以及消息的发送时间等,通常使用哈希表组织,便于快速查找。
消息tag:
在Apache RocketMQ中,消息Tag是一种用于消息分类的标签机制,它允许生产者在发送消息时为每条消息附加一个或多个标签。这些标签充当消息的二级分类,使得消费者可以根据特定的Tag来过滤和订阅消息,从而仅接收它们感兴趣的消息类型。
具体来说,Tag的作用和特点包括:
-
消息分类:Tag为消息提供了一种灵活的分类方式,使得相同Topic下的消息可以根据业务需求进一步细分。例如,在一个电商系统中,对于订单Topic,可以使用不同的Tag来区分“订单创建”、“订单付款”、“订单发货”等不同状态的消息。
-
消息过滤:消费者可以在订阅Topic时指定Tag表达式(如单个Tag或Tag的集合),RocketMQ会根据这些规则自动过滤消息,仅将符合Tag条件的消息投递给消费者。这种方式可以减少不必要的消息处理,提高消费效率。
-
资源隔离:Tag还能够在Topic内部实现消息的逻辑隔离,有助于实现更细粒度的消息管理和权限控制。
-
灵活性与扩展性:随着业务的发展,新增或修改Tag比创建新的Topic更为灵活,减少了系统架构的复杂性,提高了系统的扩展性。