二

Java 并发编程

  1. CyclicBarrier、Semaphore、Exchanger 的作用与区别
  2. CyclicBarrier 和 CountDownLatch 的区别
  3. 线程池的七个核心参数与工作流程
  4. 四种常见线程池的特点与使用风险

RAG 企业知识库

  1. 文档如何存入向量数据库
  2. RAG 文档切分粒度如何确定
  3. RAG 如何缓解文档切片造成的语义割裂

Spring 框架

  1. Spring Bean 的生命周期
  2. Spring Bean 的作用域
  3. Spring 三级缓存与循环依赖

一、Java 并发编程

01. CyclicBarrier、Semaphore、Exchanger 分别是什么?

面试口述版

这三个都是 JUC 里的并发工具类,不过解决的问题不同。我一般把它们理解为:CyclicBarrier 等线程到齐、Semaphore 控制并发数量、Exchanger 负责两个线程交换数据。

CyclicBarrier 是循环屏障。比如三个线程并行处理数据,每个线程做完一个阶段后调用 await(),先到的线程等待,直到三个线程都到达屏障,才继续进入下一阶段。正常完成一轮后,屏障还能重复使用。

Semaphore 是信号量,内部维护许可证。比如某个接口只允许三个请求同时执行,就可以创建三个许可;进入业务前调用 acquire() 获取,执行后调用 release() 归还,限制同时访问资源的线程数量。

Exchanger 用于两个线程交换数据。双方各自调用 exchange(),成功配对后拿到对方的数据,适合两个线程在特定同步点协作的场景。

回答骨架:四问总结

回答维度本题关键点
① 是什么三者都是 JUC 并发工具:CyclicBarrier 等线程到齐;Semaphore 控制许可数;Exchanger 交换两线程数据。
② 怎么实现CyclicBarrier 按参与方计数放行;Semaphore 获取和归还许可;Exchanger 在配对点交换双方传入的对象。
③ 怎么使用await:分阶段协作;acquire/release:限制同时调用外部接口的数量;exchange:双线程交换缓冲区。
④ 注意什么屏障可能 broken;许可只在成功获取后释放;交换双方没配对时可能长时间等待。

关键词记忆卡: 等到齐 → 许可证 → 两两交换 → await/acquire/exchange → 异常与超时。

面试官追问与参考回答

  1. CyclicBarrier 有线程超时或被中断会怎样?
    答: 当前等待阶段的屏障通常会被打破。超时方可能抛 TimeoutException,被中断的线程抛 InterruptedException,其他等待线程会收到 BrokenBarrierException。后续需要协调恢复,必要时重建屏障,而不是认为它能无条件复用。
  2. Semaphore 为什么要在 finally 中释放许可?
    答: 业务执行中可能抛异常,finally 能避免许可泄漏。但只有成功 acquire 或 tryAcquire 之后才释放,不能获取失败也执行 release,否则会错误增加许可。
  3. Exchanger 和 BlockingQueue 有什么区别?
    答: Exchanger 要求两个线程在交换点配对并互换数据;BlockingQueue 则是生产者放入、消费者取出,两者不需要同时到达。普通生产消费更常使用 BlockingQueue。

02. CyclicBarrier 和 CountDownLatch 有什么区别?

面试口述版

这两个工具都能让线程等待,不过侧重点不同。CountDownLatch 更像等待一组任务完成;CyclicBarrier 更像一组线程在某个阶段互相等待。

比如三个异步任务结束后,主线程才能汇总结果,就可以创建计数为 3 的 CountDownLatch:任务完成时调用 countDown(),主线程调用 await() 等待。计数降为零后,等待结束,这个计数不能再重新设置。

如果是三个工作线程需要分阶段执行任务,每个线程都在阶段末尾调用 CyclicBarrier 的 await(),全部到齐后一起进入下一阶段,这种场景更适合 CyclicBarrier。它在正常完成同步后可以重复使用。

另外,CountDownLatch 的等待方和减计数方不必是同一批线程;CyclicBarrier 通常是参与协作的线程自己互相等待。

回答骨架:四问总结

回答维度本题关键点
① 是什么CountDownLatch 等计数归零;CyclicBarrier 等参与线程到齐。
② 怎么实现Latch 由工作方 countDown、等待方 await;Barrier 通常各参与者都 await,最后一个到达后放行。
③ 怎么使用主线程等并行任务全部完成用 Latch;多个工作线程分阶段同步用 Barrier。
④ 注意什么Latch 计数不可重置;Barrier 正常完成后可循环使用,但可能发生超时、异常和屏障损坏。

关键词记忆卡: 等计数归零 → 等人数到齐 → countDown/await → 一次性/可循环 → 阶段同步。

面试官追问与参考回答

  1. CountDownLatch 能重置计数吗?
    答: 不能。计数到零后,后续 await 会直接通过。需要下一轮等待时,通常创建新的 CountDownLatch。
  2. CyclicBarrier 少一个线程一直不来怎么办?
    答: 无超时的 await 会继续等待;实际使用可以设置超时,并处理 TimeoutException、BrokenBarrierException 等异常,防止无限等待。
  3. barrierAction 在什么时候执行?
    答: 指定数量的参与线程到达后,由最后一个到达屏障的线程在放行其他线程之前执行一次,可以用于阶段汇总。如果动作抛异常,屏障会进入损坏状态。

03. 什么是线程池?七个核心参数和工作流程是什么?

面试口述版

线程池就是统一管理、复用工作线程来执行任务的机制。它不只是避免频繁创建和销毁线程,还能控制并发规模、管理任务排队,并在无法承接新任务时执行拒绝策略。

ThreadPoolExecutor 有七个核心参数:

  1. corePoolSize:核心线程数。
  2. maximumPoolSize:最大线程数。
  3. keepAliveTime:空闲线程存活时间。
  4. unit:存活时间单位。
  5. workQueue:任务等待队列。
  6. threadFactory:线程创建工厂。
  7. handler:拒绝策略。

工作流程可以记成:先核心线程、再工作队列、队列放不下再扩容、最后触发拒绝策略。

具体来说,任务提交时,如果当前工作线程数小于核心线程数,优先创建线程执行;达到核心数后,先尝试入队。入队失败且未达到最大线程数时,继续创建线程;如果无法入队也无法扩容,就触发拒绝策略。线程池关闭后也可能拒绝新任务。

任务减少后,超过核心数的工作线程如果空闲超过存活时间,通常会被回收;核心线程默认保留,除非允许核心线程超时。

回答骨架:四问总结

回答维度本题关键点
① 是什么复用并统一管理工作线程,限制资源消耗、管理任务队列。
② 怎么实现corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler;先核心、再队列、再扩容、最后拒绝。
③ 怎么使用根据任务 CPU/IO 特性、下游承载和压测结果设置线程数、队列容量、拒绝策略与监控。
④ 注意什么无界队列可能使线程池不扩容;非核心线程需空闲超时才回收;核心线程默认保留;关闭后也可能拒绝任务。

关键词记忆卡: 线程复用 → 七参数 → 核心/队列/扩容/拒绝 → 有界队列 → 空闲回收。

面试官追问与参考回答

  1. 四种常见拒绝策略是什么?
    答: AbortPolicy 直接抛异常(默认);CallerRunsPolicy 在适当条件下由提交线程执行,形成一定背压;DiscardPolicy 丢弃当前任务;DiscardOldestPolicy 尝试丢弃队列中等待最久的任务并重试。能否丢任务必须根据业务决定。
  2. 核心数 10、最大数 200,为什么可能始终只有 10 个线程?
    答: 如果使用近似无界的工作队列,达到核心数后任务通常直接入队,队列不满就不会走新增非核心线程的路径。
  3. 线程池参数怎么设置?
    答: 先区分 CPU 密集和 IO 密集任务,再结合 CPU、内存、任务耗时、请求量和下游接口容量做压测,确定线程数、队列上限、超时、拒绝处理与监控。没有对所有业务通用的倍数公式。
  4. execute 和 submit 有何区别?
    答: execute 提交 Runnable、不返回任务结果;submit 可以返回 Future,用于获取结果、等待完成或检查执行异常。submit 中的任务异常通常封装在 Future 中,需要主动观察。

04. 常见的四种线程池有什么特点?

面试口述版

如果从 Executors 的常见工厂方法来讲,我会记住四种:固定、灵活、串行、定时。

newFixedThreadPool 是固定大小线程池,同时执行的工作线程数有固定上限,额外任务进入队列等待。但它默认使用无界队列,任务积压过多可能带来内存压力。

newCachedThreadPool 会尽量复用空闲线程,没有可复用线程时可以继续创建。适合提交量可控、执行较短的任务;如果短时间提交大量任务,线程数可能快速增长,消耗系统资源。

newSingleThreadExecutor 在同一时间只使用一个工作线程执行任务,适合需要串行执行的场景;但吞吐量有限,默认队列也可能积压。

newScheduledThreadPool 支持延迟执行、固定频率或固定延迟的周期任务,适合调度场景,但不保证严格的实时精度。

实际业务中,我更倾向于明确线程数、队列容量、线程工厂和拒绝策略,根据系统承载能力配置线程池。

回答骨架:四问总结

回答维度本题关键点
① 是什么Fixed 固定线程数;Cached 按需创建与复用;Single 串行处理;Scheduled 延迟或周期执行。
② 怎么实现Fixed 和 Single 默认排队;Cached 缺少业务上限约束;Scheduled 根据调度时间安排任务。
③ 怎么使用受控并发、短任务且提交量受控、串行任务、定时任务。生产业务通常明确配置线程池参数。
④ 注意什么无界队列可能积压;Cached 可能创建大量线程;Scheduled 不是严格实时调度器。

关键词记忆卡: Fixed 固定 → Cached 灵活 → Single 串行 → Scheduled 定时 → 队列和线程上限。

面试官追问与参考回答

  1. 为什么很多团队不直接用 Executors 默认工厂?
    答: 某些工厂使用无界队列,某些缺少合适的线程数上限,高峰期容易积压任务或耗尽资源。明确配置最大线程数、队列容量和拒绝策略,更容易与业务容量匹配;并非工厂方法本身一概不能使用。
  2. scheduleAtFixedRate 和 scheduleWithFixedDelay 有什么区别?
    答: FixedRate 按计划的固定周期安排执行;FixedDelay 则在上一次任务完成后再等待设定间隔。执行很慢时,FixedRate 的后续执行会被延后,但同一个周期任务不会因此自动并行重叠。
  3. 虚拟线程执行器和固定大小线程池一样吗?
    答: 不一样。Java 21 的 newVirtualThreadPerTaskExecutor 通常每个任务创建一个虚拟线程,不是复用固定数量平台工作线程的传统线程池。即使使用虚拟线程,也要单独限制数据库连接和外部服务调用数量。

二、RAG 企业知识库

05. 一篇文档是怎么存进向量数据库的?

面试口述版

这个问题我通常按文档解析、清洗、切分、向量化、入库这条流程来理解。

首先把 PDF、Word、Markdown 等文件解析成可处理的文本和结构化信息。扫描件可能需要 OCR,表格还需要尽量保留表头与行列关系。解析后做必要的清洗,比如去掉重复页眉、页脚和乱码。

接着按标题、段落或 Token 长度把文档切成多个 chunk,再把每个 chunk 交给 Embedding 模型生成向量。

最后把向量及其关联信息保存起来。除了向量,通常还需要保留 chunk 文本或文本引用,以及文档 ID、章节、页码、版本、权限等 metadata。这样后续既能做相似度检索,也能过滤权限、追踪来源。

查询时,将用户问题转成兼容的向量,检索相关片段,必要时重排序,再将检索内容提供给大模型组织回答。

回答骨架:四问总结

回答维度本题关键点
① 是什么把文档转成可检索的 chunk 和向量,并建立内容与来源的关联。
② 怎么实现文件解析/OCR → 清洗 → 切分 → Embedding → 保存向量与关联信息。
③ 怎么使用问题向量化后相似度检索;按 metadata 过滤;必要时重排,再将相关文本交给模型回答。
④ 注意什么权限、版本、页码要可追溯;原始文件可独立存储;更新时避免新旧 chunk 混检。

关键词记忆卡: 解析清洗 → Chunk → Embedding → 向量+Metadata → 检索追溯。

面试官追问与参考回答

  1. 为什么保存 metadata?
    答: 用文档 ID、版本、章节、页码、部门权限等字段做过滤、去重、来源引用和权限控制。只存向量很难完成这些业务需求。
  2. 文档更新后怎么办?
    答: 根据文档 ID 和版本识别旧 chunk,重新解析变化内容并更新向量索引,按方案替换或删除旧记录。需要避免旧版与新版同时被错误召回,并同步更新权限元数据。
  3. PDF 里的图片和表格怎么处理?
    答: 扫描内容使用 OCR;表格尽量保留表头、行列关系;图表可结合 OCR、视觉模型和描述生成可检索内容,并保存页码或原图引用。普通文本抽取不一定能完整理解架构图。

06. RAG 文档切分粒度怎么确定?

面试口述版

我认为切分粒度没有固定的万能值,本质上要在检索精度与上下文完整性之间做平衡。

切得太大,一个 chunk 可能包含多个主题,检索匹配不够聚焦,还会占用更多上下文;切得太小,虽然容易定位局部信息,但可能丢掉前后条件,chunk 数量也会增加。

实际落地时,我会先根据文档类型选择策略。普通文本可以先以几百到一千左右 Token 做实验基线,适当设置 Overlap;制度文档尽量保留标题和段落结构;代码按类或方法切,表格要保留表头与内容的对应关系。

如果小片段召回准确但上下文不够,可以考虑父子切分:小块负责检索,命中后返回关联的大块。最终用真实问题评估正确片段的召回效果、答案质量、耗时和成本,再迭代参数。

回答骨架:四问总结

回答维度本题关键点
① 是什么每个 chunk 有多大;要平衡检索精度和上下文完整性。
② 怎么实现大块信息完整但主题不聚焦;小块定位更细却容易丢条件和背景。
③ 怎么使用按标题段落、代码结构、表格结构切分;选基线尺寸与 Overlap;必要时父子切分。
④ 注意什么没有通用 Token 大小;用真实问题测试召回、答案质量、延迟与成本。

关键词记忆卡: 大块完整 → 小块精准 → 文档结构 → Overlap/父子切分 → 评估迭代。

面试官追问与参考回答

  1. 怎么判断切分效果?
    答: 准备一组有依据的真实问题,观察正确片段是否进入 Top-K、最终回答是否准确且有来源,同时评估响应延迟、Token 消耗和存储成本。
  2. 为什么不全部切成 100 Token?
    答: 太小可能把主语、适用条件和例外情况切开。比如只检索到“2 小时内响应”,却漏掉“仅企业客户适用”,模型容易给出不完整的结论。
  3. 父子切分是什么?
    答: 预先建立小片段与包含它的大片段的对应关系。检索时用小片段精准定位,再按关联 ID 取回大段文本补足上下文。

07. RAG 如何缓解文档切片造成的语义割裂?

面试口述版

我一般从两个阶段考虑:切分时尽量保持完整语义,检索命中后补足缺失的上下文。

切分阶段可以使用 Overlap,让相邻 chunk 有一部分重叠,降低句子跨越边界时的信息丢失;还可以尽量按照标题、段落、完整句子等自然边界切分,而不是机械地按固定字符数截断。

检索阶段则可以采用句子窗口或父子切分。比如命中一个小片段后,再取它前后的句子,或者根据关联 ID 找到对应的大块,将更完整的语境交给模型。

对于离开上下文就难以理解的 chunk,还可以在建立索引前补充必要的章节背景说明。最终还是要用实际检索效果判断,因为过多重叠也会带来重复索引、重复召回和成本增加。

回答骨架:四问总结

回答维度本题关键点
① 是什么文本被分到不同 chunk,导致条件、定义或上下文丢失。
② 怎么实现切分时按语义边界和 Overlap 减少割裂;检索后用窗口、父子块补上下文。
③ 怎么使用标题/段落切分、句子窗口、父子检索、必要的上下文化背景说明。
④ 注意什么Overlap 过大会重复索引与召回;自动补背景必须忠实原文;效果依赖评估。

关键词记忆卡: 语义边界 → Overlap → 句子窗口 → 父子切分 → Contextual Retrieval。

面试官追问与参考回答

  1. Overlap 越大越好吗?
    答: 不是。重叠能降低边界信息丢失,但太大可能重复向量化同一内容,增加成本、制造重复召回,还会占用模型上下文。
  2. 句子窗口和父子切分差别是什么?
    答: 句子窗口在命中句子后动态取前后若干句;父子切分则先建立小块与大块的关联,命中小块后取它所属的大块。前者按邻近位置补,后者按预设结构补。
  3. Contextual Retrieval 是什么?
    答: 在建立检索索引前,给独立的 chunk 补充忠实原文的简短背景,比如所属章节或主题,减少片段被抽离后“这项规定”指代不明的问题。补充内容不能凭空编造。

三、Spring 框架

08. Spring Bean 的生命周期是什么?

面试口述版

Bean 的生命周期就是一个 Bean 从 Spring 容器创建、初始化、投入使用,到最终销毁的全过程。

主线可以记成:实例化 → 属性赋值和依赖注入 → Aware 回调 → 初始化前处理 → 初始化 → 初始化后处理 → 使用 → 销毁。

具体来说,Spring 先创建 Bean 实例,再注入依赖;如果 Bean 实现了相关 Aware 接口,容器会在对应时机提供 Bean 名称或容器信息。初始化前后,BeanPostProcessor 可以插入扩展逻辑;初始化回调常见的有 @PostConstruct、afterPropertiesSet() 和自定义 init-method。

初始化完成后,Bean 对外提供服务。对于需要 AOP 增强的 Bean,最终对外使用的可能是代理对象。容器关闭时,针对由容器负责管理销毁的 Bean,会执行 @PreDestroy、destroy() 等回调来释放资源。

回答骨架:四问总结

回答维度本题关键点
① 是什么从容器创建 Bean,到初始化、使用、销毁的过程。
② 怎么实现实例化 → 属性注入 → Aware → 初始化前处理 → 初始化回调 → 初始化后处理 → 使用 → 销毁。
③ 怎么使用@PostConstruct、InitializingBean、init-method、BeanPostProcessor、@PreDestroy。
④ 注意什么AOP 可能返回代理;prototype 的销毁通常不由容器自动完整管理。

关键词记忆卡: 实例化 → 依赖注入 → 前后置处理 → 初始化回调 → 使用与销毁。

面试官追问与参考回答

  1. @PostConstruct、afterPropertiesSet、init-method 的顺序是什么?
    答: 通常先执行 @PostConstruct,再执行 InitializingBean.afterPropertiesSet,最后是自定义 init-method。@PostConstruct 一般通过初始化前置处理机制触发。
  2. BeanPostProcessor 有什么用?
    答: 它允许 Spring 在 Bean 初始化前后扩展处理,例如解析注解、检查对象或进行代理增强。AOP 的部分代理创建机制也依赖相关后置处理器。
  3. 所有 Bean 都会在容器关闭时执行销毁方法吗?
    答: 不能这么说。singleton 通常由容器管理销毁回调;prototype Bean 创建后一般交给使用方,容器通常不会自动执行其完整销毁流程。

09. Spring Bean 有哪些作用域?

面试口述版

Bean 的作用域决定了 Spring 在什么范围内创建、复用和管理同一个 Bean 实例。

最常见的是 singleton 和 prototype。singleton 是默认作用域,同一个容器中的同一个 Bean 定义通常只有一个实例,所以很多无状态 Service 都使用它;prototype 则是在每次向容器请求时创建新的实例。

在 Web 应用中,还有 request、session、application 和 websocket 等作用域,分别与 HTTP 请求、会话、ServletContext 和 WebSocket 会话关联。

这里特别要注意:Spring 的 singleton 不是整个 JVM 全局唯一,也不自动保证线程安全。如果单例 Service 保存了多个请求共享的可变状态,仍然可能出现并发问题。

回答骨架:四问总结

回答维度本题关键点
① 是什么决定一个 Bean 实例在什么范围内创建、共享和管理。
② 怎么实现singleton 容器内共享;prototype 每次获取新建;request、session、application、websocket 与 Web 上下文关联。
③ 怎么使用无状态 Service 常用 singleton;确需不同实例时用 prototype;请求/会话状态考虑 Web scope。
④ 注意什么singleton 不代表全 JVM 唯一或线程安全;singleton 注入的 prototype 不会每次方法调用自动新建。

关键词记忆卡: singleton → prototype → Web scopes → 线程安全 → 按需获取。

面试官追问与参考回答

  1. singleton Bean 一定线程安全吗?
    答: 不一定。无状态的单例 Service 通常比较容易共享;但如果把不同请求的数据放在可变成员变量里,多个线程可能发生数据覆盖或竞态。
  2. singleton 中注入 prototype,是否每次调用都新建?
    答: 普通依赖注入通常在 singleton 创建时完成,所以那份 prototype 依赖通常固定下来。需要每次新建时可以按需使用 ObjectProvider、@Lookup 等机制。
  3. request 作用域在普通非 Web 容器中可以直接使用吗?
    答: 通常不可以。它依赖当前 HTTP 请求上下文,需要相应的 Web 环境和 scope 支持。

10. 什么是 Spring 的三级缓存?为什么需要三级?

面试口述版

Spring 三级缓存主要与单例 Bean 的早期引用、特定情况下的循环依赖处理有关。比如 A 通过属性注入依赖 B,B 又依赖 A,Spring 可以在 A 尚未完全初始化时提前暴露 A 的引用,让 B 有机会完成创建。

三个缓存分别是:

  1. 一级 singletonObjects: 保存完成创建的单例 Bean。
  2. 二级 earlySingletonObjects: 保存已经生成的早期单例引用。
  3. 三级 singletonFactories: 保存用于按需获取早期引用的 ObjectFactory。

典型过程是:Spring 先实例化 A,把获取 A 早期引用的工厂放进三级缓存;A 需要 B,于是创建 B;B 又需要 A,就可以从缓存获取 A 的早期引用,从而完成依赖注入。随后 B 完成创建,A 继续完成初始化,最终完整的单例进入一级缓存。

第三级缓存的价值不仅是“多存一层”,更重要的是提供按需创建早期引用的机会。涉及 AOP 时,这个早期引用可能需要是代理对象,避免依赖方拿到原始对象,而容器最终使用代理对象。

但这不代表所有循环依赖都能解决。构造器互相依赖、prototype 循环依赖,以及配置禁止相关处理时,不能简单依靠三级缓存解决。实际项目应该优先消除不合理的循环依赖。

回答骨架:四问总结

回答维度本题关键点
① 是什么通过提前暴露特定单例 Bean 的早期引用,支持部分属性注入循环依赖。
② 怎么实现singletonObjects 存成品;earlySingletonObjects 存早期引用;singletonFactories 存获取早期引用的工厂。先创建 A、暴露工厂,创建 B 时取 A 的早期引用,最后完成 A。
③ 怎么使用三级工厂允许按需获取早期对象或代理引用,二级缓存已获取的早期引用,一级最终保存完整单例。
④ 注意什么构造器循环依赖、prototype 循环依赖及禁止循环引用的配置下不能简单依赖这套机制;设计上应避免循环依赖。

关键词记忆卡: 一级成品 → 二级早期引用 → 三级工厂 → A/B 循环 → AOP 早期代理。

面试官追问与参考回答

  1. 为什么需要三级缓存,两级不行吗?
    答: 对最简单的原始对象相互引用,两层可能足够;但涉及 AOP 时,Spring 需要在合适时机通过工厂按需生成或取得早期代理引用,再缓存已生成的引用。三级缓存提供了这种延迟获取能力,避免过早暴露不合适的原始对象。
  2. 为什么构造器循环依赖通常解决不了?
    答: 创建 A 的构造器先需要 B,创建 B 的构造器又先需要 A,此时 A 连实例都没有完成,无法提前暴露可用引用,因此无法走属性注入式的早期引用流程。
  3. 三个缓存通常按什么顺序查找?
    答: 先查一级的完整单例;如果 Bean 正在创建,再尝试二级的早期引用;允许提前引用且二级没有时再使用三级工厂。取得早期引用后通常转存二级,最终创建完成后进入一级并清理早期缓存。

复习使用方法

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
墨白吖
作者分享
1. 什么是流式对话?WebSocket 和流式对话有什么区别? 2.在 RAG 中 Embedding 究竟是什么?如何选择和评估一个 Embedding 模型? 3.什么是AOP?谈谈你对AOP的理解? 4.线程池中 submit() 和 execute() 方法有什么区别? 5.如何合理分配线程池大小?
2
今天学习 并发4道 1.分别介绍一下CyclicBarrier,Semaphore,Exchanger? 2.CyclicBarrier和CountDownLatch有什么区别? 3.什么是线程池?线程池7核心参数有哪些? 4.常用线程池的区别以及特点? Rag 3 道 5.文档是怎么存进向量库的? 6.粒度怎么定? 7.怎么规避语义被切割的问题? 框架 3 道 8.Bean的生命周期是什么? 9.Bean 的作用域有哪些? 10.什么是Spring 的三级缓存?
3
昨天的答案,如果有路过好心人看到是不对的欢迎批评指正 ## 一、CAS 是什么? ### 答案 CAS 的全称是 Compare-And-Swap,也就是比较并交换。 它的核心思路是:先比较内存中的当前值和预期值是否一致。如果一致,就把它更新成新的值;如果不一致,说明当前值可能已经被其他线程修改了,这次更新就会失败。 Java 中很多原子类,比如 AtomicInteger,底层就会使用 CAS 实现原子更新操作。它的优点是,在合适的场景下,可以不通过传统的加锁方式来完成更新。 不过,CAS 也有一定的局限,比如 ABA 问题、高并发竞争下反复重试会消耗 CPU,而且它不能直接保证多个独立变量一起更新的原子性。 ------ ## 二、CAS 会有什么问题? ### 答案 我理解 CAS 主要有三个问题。 第一个是 **ABA 问题**。比如一个变量原来是 A,后来被修改成 B,又变回了 A。CAS 只比较当前值和预期值,如果发现当前值又是 A,就可能认为它没有发生变化,但实际上它中间已经被修改过了。可以使用 AtomicStampedReference,通过增加版本戳来辅助解决这个问题。 第二个是 **自旋重试带来的 CPU 开销**。如果多个线程同时竞争一个变量,CAS 失败后不断重试,就会消耗额外的 CPU 资源。 第三个是 **原子性范围的问题**。CAS 本身可以保证一次比较和更新操作的原子性,但不能直接保证多个独立变量作为一个整体更新的原子性。 如果业务需要同时修改多个字段,可以考虑使用 synchronized、Lock,或者把多个字段封装成一个不可变对象,再通过 AtomicReference 原子地替换整个对象引用。 **CAS 本身是原子的,但不代表整个业务操作都是原子的。** ------ ## 三、CountDownLatch 是干什么的? ### 答案 CountDownLatch 是 JUC 并发工具包中的一个同步工具,主要用来协调多个线程之间的执行顺序。 比如,主线程启动了三个任务,希望等这三个任务全部执行完成之后,再继续往下执行,就可以使用 CountDownLatch。 创建的时候,需要传入一个初始计数,例如 `new CountDownLatch(3)`。每个任务完成后调用 `countDown()`,让计数减一;需要等待的线程调用 `await()`。当计数减到零时,等待的线程就可以继续执行了。 它比较适合等待多个并发任务完成的场景。不过,它是一次性的,计数归零以后不能重新设置计数继续使用。 ------ ## 四、Java 原子操作类都有哪些? ### 答案 Java 在 `java.util.concurrent.atomic` 包下面提供了很多原子操作类。 比较常用的有四种: - AtomicInteger:原子更新整数。 - AtomicLong:原子更新长整数。 - AtomicBoolean:原子更新布尔值。 - AtomicReference:原子更新对象引用。 除此之外,还有数组类型的原子类,比如 AtomicIntegerArray,以及带版本戳的 AtomicStampedReference。 以 AtomicInteger 为例,它提供了 `getAndIncrement()`、`incrementAndGet()` 和 `compareAndSet()` 等方法,可以在并发环境下进行原子更新。 其中,`getAndIncrement()` 是先返回旧值,再加一;`incrementAndGet()` 是先加一,再返回新值。 如果需要把多个字段作为一个整体进行更新,也可以考虑将它们封装成一个不可变对象,再通过 AtomicReference 原子地替换对象引用。 ------ ## 五、什么是 RAG? ### 答案 RAG 的全称是 Retrieval-Augmented Generation,中文叫检索增强生成。 我理解它主要是为了解决大模型无法直接获取企业内部私有资料,以及难以及时掌握最新业务信息的问题。 比如,我们要做一个企业知识库问答系统。当用户提出一个公司内部的业务问题时,系统会先从知识库中检索相关的文档片段,再把这些片段和用户的问题一起交给大模型,让大模型结合检索到的资料生成回答。 所以,RAG 的核心就是**先检索,再生成**。 这样可以让大模型在回答问题时参考企业自己的资料,也可以返回相关的文档来源,方便用户核对。 不过,RAG 并不能保证回答一定正确,最终效果还取决于文档解析、切片质量、检索效果以及大模型的生成能力。 另外,RAG 通常不需要重新训练大模型,而是在回答问题时动态提供外部知识。**微调主要是调整模型参数,RAG 主要是让模型在回答时参考外部检索到的知识。** ------ ## 六、RAG 的完整工作流程是什么? ### 答案 我一般会把 RAG 的工作流程分成两个阶段:**离线建库和在线问答**。 首先是离线建库阶段。 我们先读取不同格式的文档,比如 PDF、Word、Markdown 等,然后进行文档解析和内容清洗,再按照一定的策略把文档切分成多个文本片段。 接着,使用 Embedding 模型把这些文本片段转换成向量,最后将向量、原始文本和相关元数据一起存储到向量数据库中。 然后是在线问答阶段。 用户提出问题后,系统可以根据需要对问题进行改写,再将问题转换成向量,到向量数据库中检索相关的文本片段。 为了进一步提高检索准确性,还可以使用 Rerank 模型对初步检索结果重新排序,选出更相关的内容。 最后,把用户的问题和检索到的文本片段一起组织成 Prompt,交给大模型生成答案。如果业务有要求,还可以返回引用来源。 所以整体可以概括为:**离线解析文档、切片、向量化和存储;在线处理问题、检索相关内容,再增强生成。** ------ ## 七、RAG 的文档切分策略有哪些? ### 答案 文档切分是 RAG 中比较重要的一步,因为切分质量会直接影响后续的检索效果。 我了解的常见策略主要有几种。 第一种是**固定大小切分**,可以按照字符数或者 Token 数进行切分。这种方式实现比较简单,但是如果直接硬切,可能会把一句话或者一段完整的意思切断,所以通常会设置一定的重叠区域,也就是 Overlap。 第二种是**按照文档结构或语义边界切分**,比如优先按照标题、段落、句子进行切分,尽量让每个片段表达相对完整的意思。 第三种是**针对特殊内容进行切分**。比如代码可以按照类、方法或者函数进行切分;表格则要尽量保留表头和行列之间的对应关系。 另外,还有父子切分策略。可以用较小的片段进行向量检索,提高定位的精度,再根据关联关系取出包含更多上下文的较大片段交给大模型。 根据文档类型、检索效果和模型上下文长度进行选择。 ------ ## 八、@Autowired 和 @Resource 有什么区别? ### 答案 这两个注解都可以用来实现依赖注入,主要区别在于来源和默认的依赖查找方式。 @Autowired 是 Spring 提供的注解,默认主要按照类型匹配 Bean。如果同一个类型有多个 Bean,就可以使用 @Qualifier 指定具体要注入哪一个。 @Resource 来自 Java 的通用注解规范,在现代项目中通常使用 `javax.annotation.Resource` 或 `jakarta.annotation.Resource`,具体取决于项目版本。 在 Spring 中,@Resource 通常优先按照名称查找 Bean,也可以通过 `name` 属性明确指定要注入的 Bean。 ------ ## 九、Spring 中有哪些常见的设计模式? ### 答案 Spring 里面使用了不少设计模式,我比较熟悉的有以下几种。 第一种是**单例模式**。Spring 中的 Bean 默认使用 singleton 作用域,同一个 IoC 容器中的同一个 Bean 定义通常只有一个实例。 第二种是**工厂模式**。Spring 容器可以通过 BeanFactory、ApplicationContext 等机制创建和管理 Bean,我们不需要在业务代码里到处手动创建对象。 第三种是**代理模式**。比如 Spring AOP 可以通过代理来实现事务、日志等横切功能,让业务代码和这些通用逻辑分离。 第四种是**模板方法模式**。比如 JdbcTemplate 把一些重复的数据库操作流程封装起来,让我们更多地关注具体的 SQL 和业务逻辑。 第五种是**观察者模式**。Spring 的事件发布和监听机制就体现了这种模式,一个组件发布事件,其他监听器可以接收并处理事件。 这些设计模式的目的都是让代码职责更清晰,降低耦合,提高复用性和扩展性。 需要注意的是,Spring Bean 默认是单例,并不代表所有 Bean 永远都是单例。Bean 也可以配置成其他作用域,比如 prototype、request 和 session。 ------ ## 十、什么是 IoC? ### 答案 IoC 的全称是 Inversion of Control,也就是控制反转。 我理解它的核心就是把对象的创建和依赖关系管理交给容器,而不是完全由业务代码自己控制。 比如,传统开发中,一个 Service 需要使用另一个对象,可能会自己通过 `new` 创建它。这样对象之间容易产生比较强的耦合。 使用 Spring IoC 之后,我们可以把类交给 Spring 容器管理,由容器负责创建 Bean、注入依赖,并按照相应的配置管理 Bean 的生命周期。 这样,如果以后需要替换某个依赖的实现,通常可以通过调整注入关系或相关配置来完成,而不需要在业务代码中到处修改对象的创建逻辑。 Spring 中主要通过依赖注入,也就是 DI,来实现 IoC。 所以可以这样理解:**IoC 是一种设计思想,DI 是实现这种思想的主要方式之一。**
3
今天的学习 1.cas 是什么? 2.cas会有什么问题? 3.并发工具类CountDownlatch 是干什么的? 4.原子操作类都有什么? 5.什么是RAG? 6.RAG的工作流程是什么? 7.文档切割策略都有哪些? 8.@Autowired 和@Resource 注解的区别? 9.Spring 中的设计模式有哪些? 10.什么是 IOC? 唉,我也不知道学这些还有没有用,是不是有点过时
3
我好迷茫啊,这也面不过,那也面不过的,hhh 但是每天也跟着学,这也学那也学 这也不会,那也不会的😂
4
下载 APP