组合、代理与事件模式

共 22 题
📑 题目列表 22 题
#
★★★

1. 装饰器模式的组合顺序中多个装饰器叠加时执行顺序如何决定(后加的在外层先执行),与责任链模式在'顺序敏感'上的差异?

解释装饰器模式的组合顺序:多个装饰器叠加时执行顺序如何决定(后加的在外层先执行),与责任链模式在"顺序敏感"上的差异?

  • 装饰器嵌套时内层先执行、外层后执行
  • 执行顺序由包裹顺序决定
  • 与责任链(链式传递、可中断)的差异

装饰器模式通过嵌套包裹改变组件行为,执行顺序由"包裹顺序"决定:后加的装饰器在最外层,方法调用时先进入外层装饰器,再逐层进入内层,最后到达被装饰组件;返回时方向相反。例如 decorator_A 包裹 decorator_B 包裹 component,调用时执行顺序是 A 的前置逻辑 → B 的前置逻辑 → component → B 的后置逻辑 → A 的后置逻辑。因此"后加的在外层先执行"。与责任链的差异:装饰器是"一定全部执行、嵌套全包"(每个装饰器都包裹并一定调用下一层),顺序固定且所有装饰器都生效;责任链是"链式传递、可中断"(处理器按链顺序尝试,可处理并终止,不一定所有处理器都执行),顺序敏感在于"谁先处理谁决定职责归属",且可动态选择是否继续。责任链更强调"职责的逐步传递与可中断",装饰器更强调"功能的叠加包裹"。

核心是"叠加包裹顺序" vs "链式传递顺序"。装饰器天然嵌套、全执行、顺序由包裹决定;责任链线性传递、可中断、顺序决定职责归属。理解"装饰器固定全执行、责任链可中断"是关键差异。

#
★★★

2. 组合模式的统一接口中如何让叶子与容器实现同一接口以支持递归处理,遍历统计与访问者模式(Visitor)在职责上的分工?

解释组合模式的统一接口:如何让叶子与容器实现同一接口以支持递归处理,遍历统计与访问者模式(Visitor)在职责上的分工?

  • 叶子与容器统一接口(Component)
  • 递归处理(树遍历)
  • 组合负责结构遍历、Visitor 负责操作扩展

组合模式让叶子(Leaf)与容器(Composite)实现同一个 Component 接口(如 getSize、add、remove、forEach),容器内部持有子 Component 列表,操作通过递归调用子节点实现,从而对客户端透明——无论叶子还是容器,客户端都按同一接口处理,递归遍历整棵树。遍历统计(如计算总大小、节点数)在容器内递归聚合子节点结果。访问者模式(Visitor)与组合的分工:组合负责"结构"(树形组织与递归遍历本身),Visitor 负责"操作"(在遍历时对不同类型的节点执行不同逻辑),两者结合——通过 accept(visitor) 让每个节点调用 visitor 的对应 visit 方法,实现"在不修改节点类的前提下为结构添加新操作"。组合提供结构,Visitor 提供可扩展的操作,职责分离。

核心是"统一接口 + 递归 + 操作与结构分离"。组合模式解决"树形结构的统一处理",Visitor 解决"在结构上追加新操作而不改节点类"。理解"组合管结构、Visitor 管操作扩展"的分工是关键。

#
★★★

3. 组合模式在文件系统/UI 组件树中的应用中如何统一文件与目录接口,递归计算大小/统计数量,与迭代器模式的配合?

解释组合模式在文件系统/UI 组件树中的应用:如何统一文件与目录接口,递归计算大小/统计数量,与迭代器模式的配合?

  • 文件与目录的统一接口(FileSystemNode)
  • 递归计算大小/统计数量
  • 与迭代器配合遍历树

文件系统把"文件"与"目录"统一成 FileSystemNode 接口(如 getSize()、getName()、listFiles()),文件是叶子(getSize 返回自身大小),目录是容器(getSize 递归求和子节点大小,listFiles 返回子节点),客户端用统一接口处理,透明地递归遍历。UI 组件树同理,Component 统一"按钮/文本框/容器",容器递归渲染/布局子组件。递归计算大小/统计数量:在容器实现中递归调用子节点的 getSize 或 count,聚合结果。与迭代器模式的配合:迭代器提供对树结构的统一遍历(前序/后序/深度优先),客户端用迭代器 forEach 遍历节点而不关心节点是叶子还是容器;组合模式提供"结构",迭代器提供"遍历方式",两者配合让客户端以统一方式访问树的所有节点。

核心是"统一接口 + 递归 + 遍历抽象"。文件/目录统一接口让递归计算透明,迭代器提供统一遍历,组合管结构、迭代器管遍历。理解"组合解决结构、迭代器解决遍历"是关键。

#
★★

4. 同步代理(Synchronization Proxy)的实现中如何用代理在方法调用前后统一加锁实现线程安全,与直接同步方法在可测试性上的差异?

解释同步代理(Synchronization Proxy)的实现:如何用代理在方法调用前后统一加锁实现线程安全,与直接同步方法在可测试性上的差异?

  • 代理统一加锁/解锁
  • 线程安全与业务逻辑分离
  • 可测试性与可复用性差异

同步代理(Synchronization Proxy)在代理对象中包裹真实对象,在代理的每个方法调用前后统一加锁/解锁,把"线程安全"横切关注点从业务逻辑中分离,保证真实对象的方法被串行/安全地访问。实现:代理类与被代理类实现同一接口,代理方法获取锁→调用真实对象方法→释放锁(可用 try/finally 保证释放)。与直接同步方法(在方法上直接加 synchronized)的差异:直接同步把锁逻辑耦合进业务方法,无法在不改业务的情况下切换锁策略;同步代理把锁逻辑集中到代理,业务对象保持纯净,易于测试(能用不带锁的 mock 直接测业务逻辑)、可复用(同一业务对象可用不同代理:同步代理、日志代理、远程代理等)。可测试性上,同步代理让"业务逻辑测试"与"并发控制测试"分离,更清晰。

核心是"横切关注点的分离"。代理把锁逻辑从业务中抽出,业务对象纯净、可复用、可测试。这是"代理控制访问"与"装饰器增强功能"意图差异的体现之一。

#
★★

5. Thread Pool 线程池模式中核心线程数、最大线程数、任务队列选型(有界/无界/同步移交)、拒绝策略(Abort/CallerRuns/Discard)的工程取舍

解释 Thread Pool 线程池模式:核心线程数、最大线程数、任务队列选型(有界/无界/同步移交)、拒绝策略(Abort/CallerRuns/Discard)的工程取舍?

  • 核心线程数、最大线程数的配置
  • 队列选型(有界/无界/同步移交)的背压
  • 拒绝策略的取舍

线程池配置的工程取舍:核心线程数(常态保持的线程数)决定基础并发能力,最大线程数决定峰值并发上限,超过最大线程数后任务进入队列。队列选型:有界队列(如 ArrayBlockingQueue)提供背压,队列满后触发拒绝策略,防止内存溢出,适合保护下游;无界队列(如 LinkedBlockingQueue)不会拒绝,但任务可无限堆积导致内存溢出,只在任务量可控时用;同步移交(SynchronousQueue)不缓存任务,直接交给线程,适合"任务必须立即执行"或"线程数=任务数"(如 newCachedThreadPool)的场景。拒绝策略:Abort(抛异常,默认)快速失败;CallerRuns(调用者线程执行)提供背压、不丢任务但阻塞调用者;Discard/DiscardOldest 静默丢弃,适合可容忍丢失的任务。选取原则:核心线程数≈CPU 密集用 CPU 核数、IO 密集用更高;最大线程数受资源上限约束;队列按"能否容忍堆积/丢弃"选;拒绝策略按"能否容忍失败/阻塞"选。

核心是"线程数、队列、拒绝策略"的三维权衡。有界队列+拒绝策略提供背压保护,无界队列简单但风险大,同步移交适合 cached 场景。理解"背压 vs 吞吐 vs 丢弃"是关键。

#
★★

6. 享元模式的内在状态(intrinsic,可共享)与外在状态(extrinsic,由客户端传入)划分,以及多线程下享元池的线程安全策略

解释享元模式的内在状态与外在状态划分,以及多线程下享元池的线程安全策略?

  • 内在状态(可共享)与外在状态(客户端传入)的划分
  • 外在状态由客户端传入、不共享
  • 享元池的并发安全(线程安全池、隔离)

享元模式把对象状态分为内在状态(intrinsic)与外在状态(extrinsic)。内在状态是对象不变、可共享的部分(如字体、颜色、字符本身),存储在享元对象中,多个上下文共享同一享元;外在状态是随上下文变化、由客户端传入的部分(如字符的位置、大小),不存储在享元对象中,由调用方在使用时传入。划分原则:可共享且不变的状态放享元(内在),随上下文变化的状态由客户端持有(外在)。多线程下享元池的线程安全:享元池本身是共享资源,需用线程安全容器(如 ConcurrentHashMap、池化)或加锁保护创建/获取;享元对象若含可变状态则无法安全共享,通常享元对象是不可变的(内在状态 final),外在状态由线程各自持有,因此多线程并发使用时互不干扰。若要共享可变状态,需同步或隔离。

核心是"不变的可共享 vs 变化的由客户端传入"。享元对象不可变(内在 final)才能在多线程安全共享,外在状态线程本地持有。理解"状态划分决定能否共享"是关键。

#
★★

7. 享元模式的适用边界中什么场景下共享反而得不偿失(对象创建便宜、状态易变、共享状态需同步),如何判断是否值得引入?

解释享元模式的适用边界:什么场景下共享反而得不偿失(对象创建便宜、状态易变、共享状态需同步),如何判断是否值得引入?

  • 对象创建便宜时共享收益小
  • 状态易变时共享引入同步开销
  • 何时值得引入

享元模式通过共享大量相似对象节省内存,但并非所有场景都值得。得不偿失的场景:1) 对象创建便宜——若对象本身很小、创建开销低,共享省下的内存与时间收益有限,引入享元反而增加复杂度;2) 状态易变——若对象状态频繁变化,共享会导致状态互相污染,需额外同步或不断复制,共享代价高;3) 共享状态需同步——若必须共享可变状态并用锁保护,同步开销可抵消共享收益,甚至引入竞争。判断是否值得:评估对象数量是否巨大(千/万级)、剩余内存是否紧张、内在状态是否稳定不变、外在状态能否由客户端分离持有。若对象数量大、内在状态稳定、外在状态可分离,则值得引入;若数量少、创建便宜、状态动态,则不值得,直接新建对象更简单。

核心是"共享收益 vs 共享代价"。收益来自"大量对象+稳定内在状态+内存节省",代价来自"状态易变+同步+复杂度"。判断关键是"状态能否稳定分离"与"对象量是否足够大"。

#
★★

8. 装饰器(Decorator)与代理(Proxy)在结构相同(都持有组件引用)下的意图差异中装饰器增强功能,代理控制访问

解释装饰器(Decorator)与代理(Proxy)在结构相同(都持有组件引用)下的意图差异:装饰器增强功能,代理控制访问?

  • 两者结构上都持有被包裹对象引用
  • 装饰器增强功能、代理控制访问
  • 意图差异决定使用场景

装饰器与代理在结构上相似:都包含一个被包裹对象(组件)的引用,都实现相同接口,调用时都转发给被包裹对象。但意图不同:装饰器(Decorator)的目的是"增强功能"——在不修改原对象的前提下,动态添加职责(如加日志、加缓存、加压缩),强调"功能的叠加";代理(Proxy)的目的是"控制访问"——拦截访问以控制访问方式(如远程代理、虚拟代理、保护代理、同步代理),强调"对访问的控制与拦截"。因此装饰器通常由客户端显式组合、可多层嵌套、透明地增强;代理通常由客户端获取代理对象、代理代表真实对象、可能附加访问控制。意图差异决定场景:增强功能用装饰器,控制访问用代理。经典例子:日志装饰器 vs 权限代理。

核心是"增强 vs 控制"的意图差异。两者结构相同但语义不同:装饰器"加功能",代理"管访问"。面试常考"结构相似但意图不同",理解意图是关键。

#
★★

9. CountDownLatch 与 CyclicBarrier 中一次性门闩 vs 可复用屏障的语义差异,分别在并行任务汇聚(join)场景如何使用?

解释 CountDownLatch 与 CyclicBarrier:一次性门闩 vs 可复用屏障的语义差异,分别在并行任务汇聚(join)场景如何使用?

  • CountDownLatch 一次性、减法计数
  • CyclicBarrier 可复用、循环屏障
  • join 场景的使用

CountDownLatch 是一次性门闩(one-shot latch):初始化一个计数 N,每个线程完成任务后 countDown() 减一,主线程 await() 直到计数归零,然后放行。它只能使用一次,不可复用。用于"等待 N 个任务完成"的汇聚场景(如主线程等待多个子任务全部完成后再继续)。CyclicBarrier 是可复用屏障(cyclable barrier):初始化参与者数 N,当 N 个线程都到达屏障(await)时,同时放行,屏障重置后可再次使用(可循环)。用于"多个线程在某阶段汇聚并对齐"的场景(如多线程分阶段并行,每阶段末尾对齐)。差异:CountDownLatch 是"等待计数归零"(一次性,主等待者可在任意线程 countDown),CyclicBarrier 是"等待 N 个线程到达"(可复用,所有线程彼此等待对齐)。join 场景:CountDownLatch 适合"主线程等待一组任务完成";CyclicBarrier 适合"多线程在多个阶段反复汇聚"。

核心是"一次性 vs 可复用"、"等待计数 vs 等待对齐"。CountDownLatch 是单向等待(主线程等子任务),CyclicBarrier 是多向对齐(多个线程互相等)。理解"汇聚点是否可复用"决定用哪个。

#
★★

10. 生产者-消费者的队列选型中有界/无界队列在背压与丢弃策略上的差异,如何用信号量或容量控制生产速率?

解释生产者-消费者的队列选型:有界/无界队列在背压与丢弃策略上的差异,如何用信号量或容量控制生产速率?

  • 有界队列提供背压
  • 无界队列可能内存溢出
  • 信号量/容量控制生产速率

生产者-消费者模型中,队列选型决定背压行为。有界队列(如 ArrayBlockingQueue):队列满时生产者阻塞(put)或拒绝,提供背压,防止消费者跟不上时无限堆积,保护内存;但可能阻塞生产者,需配合丢弃/覆盖策略。无界队列(如 LinkedBlockingQueue、ConcurrentLinkedQueue):生产者永不阻塞,简单,但消费者慢时任务无限堆积导致内存溢出。控制生产速率:用信号量(Semaphore)限制"在途任务数"——生产者先 acquire 再提交,消费者完成后 release,从而限制积压量;或用容量感知(如队列剩余容量+buffer 计数)控制生产者是否继续。取舍:需要背压保护用有界队列+阻塞,需保吞吐且可容忍少量积压用较大有界队列,绝不希望阻塞但任务可控时用无界队列(有风险)。

核心是"背压 vs 吞吐/风险"。有界队列用阻塞/丢弃实现背压,无界队列简单但有溢出风险。信号量是"控制在途量"的通用手段。理解"背压来自有界+阻塞"是关键。

#
★★

11. 智能引用(Smart Reference)与引用计数中 shared_ptr 如何自动管理资源释放,循环引用与原子计数开销如何解决?

解释智能引用(Smart Reference)与引用计数:shared_ptr 如何自动管理资源释放,循环引用与原子计数开销如何解决?

  • shared_ptr 的引用计数自动释放
  • 循环引用导致的内存泄漏
  • weak_ptr 破环与原子计数开销

shared_ptr 用引用计数管理资源:每次拷贝增加引用计数,销毁时减少,计数归零时自动释放底层资源,从而避免手动 delete 的泄漏与悬垂。问题与解决:循环引用——两个对象互相持有 shared_ptr,形成环,导致引用计数永不为零,资源泄漏;解决用 weak_ptr,它不影响计数,打破环(弱引用)。原子计数开销——shared_ptr 的引用计数是原子的(多线程安全),但原子操作(及其内存屏障)有开销,高频拷贝/销毁时明显;解决:优化为"共享无锁或分离所有权"(如 unique_ptr 独占、或只在必要时用 shared_ptr)、减少指针拷贝、或用 intrusive_ptr 避免额外分配。此外 shared_ptr 还能配自定义 deleter 处理特殊资源释放。

核心是"引用计数自动释放 + 破环 + 原子开销"。shared_ptr 用计数自动管理,weak_ptr 破环,原子计数用减少拷贝/unique_ptr 优化。理解"循环引用用 weak、原子开销用最小化拷贝"是关键。

#

12. Thread-Specific Storage 线程局部存储的适用场景与内存泄漏风险中线程池中 ThreadLocal 未 remove 导致对象无法回收

解释 Thread-Specific Storage(线程局部存储)的适用场景与内存泄漏风险:线程池中 ThreadLocal 未 remove 导致对象无法回收?

  • ThreadLocal 的适用场景(每线程独立状态)
  • ThreadLocalMap 与线程绑定
  • 线程池中未 remove 导致泄漏

Thread-Specific Storage(如 Java ThreadLocal)为每个线程提供独立的变量副本,各线程访问自己的副本,互不干扰,适合"每线程独立、不共享"的状态(如线程连接的上下文、事务、线程级缓存、随机数)。实现:每个线程有自己的 ThreadLocalMap,key 是 ThreadLocal,value 是线程私有值。风险:线程池复用线程,线程不销毁,若 ThreadLocal 未在每次任务后 remove,value 会一直留在线程的 ThreadLocalMap 中,即使任务结束、对象不再需要,只要线程存活就无法回收,导致内存泄漏(尤其 value 引用大对象);且线程池线程通常长期存活,泄漏累积严重。解决:在 finally 中显式 remove,或用 try-with-resources 封装,或用可回收的 ThreadLocal(弱引用 key 但 value 仍强引用)。使用 ThreadLocal 要遵循"用完即清理"。

核心是"线程复用导致 ThreadLocal 泄漏"。ThreadLocal 的 value 绑定线程,线程池线程复用不销毁,未 remove 则 value 长期驻留无法回收。理解"线程池复用 + 未清理 = 泄漏"是关键。

#

13. 线程协作模式与并发原语(锁、信号量、条件变量)的边界差异?

解释线程协作模式(生产者-消费者、读写锁等)与并发原语(锁、信号量、条件变量)的边界差异?

  • 并发原语是底层机制
  • 协作模式是上层应用模式
  • 原语与模式的组合关系

并发原语(锁、信号量、条件变量)是底层机制,提供"互斥、同步、等待通知"等基础能力:锁(mutex)保证互斥访问共享资源;信号量(semaphore)控制有限资源的并发数量;条件变量(condition variable)让线程在条件不满足时等待、条件满足时被唤醒。线程协作模式(生产者-消费者、读写锁、屏障、线程池等)是建立在原语之上的上层应用模式,解决特定的协作问题。边界差异:原语是"构建块"(原子性、互斥、同步的底层保证),模式是"套路"(如何组合原语解决具体协作问题)。例如生产者-消费者模式用"锁+条件变量+有界队列"实现,读写锁模式本身就是一种"用锁实现的专门化互斥"。模式可以基于原语实现,也可用更高级抽象(如队列、channel)。理解"原语是底层机制、模式是上层套路"的层次关系是关键。

核心是"机制 vs 模式"的层次。原语提供互斥/同步/等待能力,模式用原语组合解决协作问题。边界在于"何时用原语、何时用模式"——简单互斥用锁,复杂协作用模式(内部仍用原语)。

#

14. 生产者消费者、读写锁等线程协作模式在生产中的典型应用?

说明生产者-消费者、读写锁等线程协作模式在生产中的典型应用?

  • 生产者-消费者的典型场景
  • 读写锁的典型场景
  • 各模式适配的业务

生产者-消费者模式:典型应用于"任务队列/消息处理"——生产线程(如接收请求、采集数据)提交任务,消费线程(如 worker 池、异步处理)从队列取任务执行,解耦生产与消费速率,常用于日志异步写入、消息队列消费、任务调度、图片/视频处理流水线。读写锁模式:典型应用于"读多写少"的共享数据——多个读者可并发读,写者独占,如配置缓存、路由表、元数据缓存、并发查询的共享状态,读并发高性能而写持锁保证一致性。其它典型:线程池(任务执行复用)、屏障(多阶段汇聚)、信号量(限制并发连接数/资源池)。这些模式在生产中解决"吞吐、并发、一致性"的平衡,是并发编程的基础组件。

核心是"模式适配业务"。生产者-消费者解耦吞吐,读写锁优化读多写少,线程池复用资源。理解"各模式解决什么问题、用在什么场景"是关键。

#

15. 线程协作有哪些常见误区(死锁、忙等、虚假唤醒)?

解释线程协作的常见误区:死锁、忙等、虚假唤醒是什么,如何避免?

  • 死锁的成因与避免
  • 忙等的资源浪费
  • 虚假唤醒与条件等待

线程协作的常见误区:1) 死锁(deadlock)——多个线程循环等待对方持有的资源,彼此阻塞。避免:以固定顺序加锁、使用超时锁、避免嵌套锁、用 tryLock 等。2) 忙等(busy wait)——线程用空循环轮询等待条件,浪费 CPU。避免:用条件变量/信号量等阻塞等待(wait/notify),让线程休眠而非空转。3) 虚假唤醒(spurious wakeup)——线程在条件未满足时被无故唤醒(wait 可能被唤醒但条件仍不成立)。避免:在循环中检查条件(while 而非 if),即 while (!cond) wait();,唤醒后重新检查条件再继续。此外还有"等待通知丢失"(先 notify 后 wait 导致错过通知)、优先级反转、饥饿等。核心是"用阻塞等待替代忙等、用循环检查处理虚假唤醒、用固定顺序避免死锁"。

核心是"三个经典误区及其对策"。死锁用加锁顺序/超时,忙等用条件变量阻塞,虚假唤醒用 while 循环重查。理解"wait 必须在循环中检查条件"是防虚假唤醒的关键。

#

16. 线程协作的核心概念中等待通知、顺序保证与公平性?

解释线程协作的核心概念:等待通知、顺序保证与公平性?

  • 等待通知(wait/notify)机制
  • 顺序保证(happens-before、顺序执行)
  • 公平性(锁的公平调度)

线程协作的核心概念:1) 等待通知(wait/notify)——线程在条件不满足时调用 wait 阻塞等待并释放锁,其它线程在条件满足时 notify 唤醒等待者,实现"条件驱动的协作"。2) 顺序保证——通过同步机制保证操作顺序(如 happens-before 关系、锁的顺序、join 等待),确保先执行的操作其结果对后执行者可见,避免指令重排与数据竞争。3) 公平性——当多个线程同时等待资源时,按什么顺序分配(如锁的公平与非公平:公平锁按先到先得,非公平锁可能插队)。等待通知解决"同步与唤醒",顺序保证解决"操作的可见性与次序",公平性解决"资源分配的次序"。三者共同构成线程协作的基础。实现时注意:wait 需在持锁下调用、用 while 循环防虚假唤醒、notify 与 notifyAll 的选择、公平性在性能与公平间权衡。

核心是"等待通知、顺序、公平"三个维度。等待通知管"何时唤醒",顺序保证管"可见性次序",公平性管"谁先获得"。理解各自解决的问题域是关键。

#

17. Go channel 的同步/带缓冲语义中如何用 channel 实现 goroutine 间通信与同步,无缓冲 channel 的收发握手如何阻塞?

解释 Go channel 的同步/带缓冲语义:如何用 channel 实现 goroutine 间通信与同步,无缓冲 channel 的收发握手如何阻塞?

  • 无缓冲 channel 的同步握手
  • 带缓冲 channel 的异步队列
  • channel 用于通信与同步

Go channel 是 goroutine 间通信与同步的机制。无缓冲 channel:发送与接收必须同时进行(同步握手)——发送 goroutine 在 channel 无接收者时阻塞等待,接收 goroutine 在 channel 无发送者时阻塞等待,两者配对后数据直接传递,实现"同步":发送方确认接收方已接收到数据,接收方确认发送方已发送,从而可用于同步。带缓冲 channel:有一个容量 N 的缓冲区,发送时若缓冲区未满则立即放入(不阻塞),满时阻塞;接收时若缓冲区非空则立即取出,空时阻塞。它实现"异步队列",用于解耦生产与消费速率。用 channel 实现同步:多个 goroutine 通过发送/接收"信号"(如空 struct)来同步,如用 channel 等待某 goroutine 完成、用带缓冲 channel 作为信号量限制并发。channel 兼具"通信"(传数据)与"同步"(握手/阻塞)两种语义。

核心是"同步握手 vs 异步缓冲"。无缓冲 channel 是同步握手(配对阻塞),带缓冲 channel 是异步队列。channel 既能传数据(通信)又能控制时序(同步)。理解"无缓冲=同步、有缓冲=异步"是关键。

#

18. Proactor 与 Reactor 的工程取舍中为什么 Windows IOCP 采用完成回调(Proactor)而 Netty 是事件分发(Reactor),两者的线程模型差异?

解释 Proactor 与 Reactor 的工程取舍:为什么 Windows IOCP 采用完成回调(Proactor)而 Netty 是事件分发(Reactor),两者的线程模型差异?

  • Reactor 事件就绪分发(同步 IO)
  • Proactor 完成回调(异步 IO)
  • 两者的线程模型与平台契合

Reactor 模式:IO 事件就绪后由事件分发器(EventLoop)检测并回调处理,read/write 由应用线程执行(同步 IO),适合 Linux 的 epoll(就绪通知)。Proactor 模式:IO 操作由内核在执行,完成后通过回调(Completion Routine)通知应用,应用无需等待 IO 完成(异步 IO),适合 Windows IOCP(异步完成端口)。为什么 IOCP 用 Proactor:Windows 的 AIO 原生支持"操作发起即在后台执行、完成回调",天然契合 Proactor;而 Linux 的 epoll 是"就绪通知"(需应用自己发起读),更适合 Reactor。Netty 用 Reactor:因为跨平台且 Linux 下 epoll 就绪模型成熟,Netty 用 EventLoop 分发事件、IO 线程执行读写。线程模型差异:Reactor 在事件循环线程中处理就绪事件,线程数少(通常=核数),读写在事件循环线程完成;Proactor 由内核执行 IO、完成回调可能在不同线程派发,线程管理更复杂但 IO 不阻塞事件循环。取舍:Reactor 简单、跨平台、Linux 友好;Proactor 在原生 AIO 平台延迟更低、不阻塞事件循环,但实现复杂、平台依赖。

核心是"就绪通知 vs 完成通知"、"同步 IO vs 异步 IO"。Reactor 等就绪后自做 IO,Proactor 等内核做完再回调。平台差异(epoll vs IOCP)决定哪种更契合。理解"Reactor 同步 IO、Proactor 异步 IO"是关键。

#

19. SynchronousQueue 的直接移交中它没有容量,生产者与消费者如何配对握手,在 newCachedThreadPool 中的作用?

解释 SynchronousQueue 的直接移交:为什么它没有容量,生产者与消费者如何配对握手,在 newCachedThreadPool 中的作用?

  • SynchronousQueue 无容量
  • 生产者消费者配对握手
  • newCachedThreadPool 用它做直接移交

SynchronousQueue 是一个"无容量"的队列:它不存储任何元素,每个 put 必须等一个 take 配对(直接移交),put 阻塞直到有消费者 take,take 阻塞直到有生产者 put。生产者与消费者通过"握手"配对:生产者把元素直接交给消费者,中间无缓冲。因此它没有内部存储,本质是"同步移交"而非"队列缓存"。在 newCachedThreadPool 中的作用:该线程池用 SynchronousQueue,意味着"任务提交后必须立即有线程执行"——若无空闲线程,则新建线程(cached 线程池可按需创建),从而保证任务不被缓存、立即执行;同时空闲线程会回收(keep-alive 超时),线程数随任务动态伸缩。SynchronousQueue 实现"即时移交 + 无限线程扩展",配合 cached 线程池实现"任务即来即走、线程按需增减"。

核心是"无容量、直接配对、即时移交"。SynchronousQueue 不缓存,生产者与消费者同步握手,cached 线程池据此动态建线程。理解"无容量 = 直接移交 = 任务不排队"是关键。

#

20. 事件循环的阻塞风险中 CPU 密集任务必须移出事件循环(如 Node worker_threads),阻塞事件循环如何延迟所有回调?

解释事件循环的阻塞风险:为什么 CPU 密集任务必须移出事件循环(如 Node worker_threads),阻塞事件循环如何延迟所有回调?

  • 事件循环单线程模型
  • 阻塞事件循环延迟所有回调
  • CPU 密集任务移出到 worker 线程

事件循环(如 Node.js 的 EventLoop)是单线程模型,所有 IO 回调、任务都在同一线程循环执行。若某个任务耗时(如 CPU 密集计算、同步阻塞 IO),会阻塞事件循环,导致后续所有回调、定时器、IO 事件都无法处理,表现为"整个进程卡住",其他请求被延迟。因此 CPU 密集任务必须移出事件循环(如 Node 的 worker_threads、或把计算任务交给后台线程/服务),让事件循环保持快速响应 IO 事件。阻塞事件循环的代价:所有回调排队延迟,心跳/超时检查失效,表现为全面延迟与假死。正确做法:事件循环只做轻量 IO 调度,CPU 密集/长时间任务用 worker 线程或异步化,避免阻塞主循环。

核心是"单线程事件循环 + 阻塞即全卡"。事件循环串行处理所有回调,慢任务会延迟所有回调。理解"CPU 密集任务必须移出事件循环"是关键。

#

21. 共享内存并发重构到 Actor 的步骤中如何识别共享状态、定义消息协议与所有权,重构中消息风暴与调试难度如何权衡?

解释共享内存并发重构到 Actor 的步骤:如何识别共享状态、定义消息协议与所有权,重构中消息风暴与调试难度如何权衡?

  • 识别共享状态与所有权
  • 定义消息协议
  • 消息风暴与调试难度权衡

把共享内存并发重构为 Actor 模型,步骤:1) 识别共享状态——找出所有被多个线程读写的可变状态,明确"谁拥有"(单一所有权),把共享状态封装进 Actor 作为其内部状态。2) 定义消息协议——为每个 Actor 定义消息类型(请求/响应/事件),明确消息的语义与不可变性(消息最好不可变,避免共享可变引用)。3) 划分 Actor 边界——每个 Actor 独占其状态,通过发送消息通信,无共享锁。权衡:消息风暴——若 Actor 之间消息过多过密(如每步都发消息),消息传递开销与队列积压会激增,性能下降;调试难度——Actor 的异步消息与并发执行使追踪调用链、重现问题比同步代码难,需借助日志/追踪。需权衡"消息粒度"(合并批量消息减少风暴)与"可调试性"(保持消息语义清晰、边界合理)。

核心是"状态封装 + 消息协议 + 所有权"。Actor 用"独占状态 + 消息通信"消除共享锁,但消息风暴与调试成本是代价。理解"消息粒度与可调试性的权衡"是关键。

#

22. 管道(Pipeline)与责任链的差异中数据流式处理 vs 请求逐级处理,流水线各阶段如何并行执行与背压控制?

解释管道(Pipeline)与责任链的差异:数据流式处理 vs 请求逐级处理,流水线各阶段如何并行执行与背压控制?

  • 管道的数据流式处理 vs 责任链的请求逐级处理
  • 各阶段并行执行
  • 背压控制

管道(Pipeline)模式把处理拆成多个阶段,数据像水一样流过各阶段,每个阶段对数据做变换并传给下一阶段,强调"数据流式处理"(数据逐阶段流动、可并行流水)。责任链(Chain of Responsibility)把请求沿链传递,每个处理器可处理、可拒绝、可传递,强调"请求逐级处理",可能在某处理器终止(不保证全部执行)。差异:管道是"数据流 + 全阶段处理 + 可并行",责任链是"请求传递 + 可中断 + 顺序决定职责"。流水线并行:各阶段可用不同线程处理,数据并行流过(生产者-消费者),同一阶段可多实例并行,提高吞吐。背压控制:用有界队列连接阶段,阶段处理慢时队列满,阻塞上游或丢弃,防止下游积压;可用信号量/限流控制数据进入速率。每阶段有独立缓冲,整体吞吐受最慢阶段(瓶颈)限制。

核心是"数据流 vs 请求链"、"全处理 vs 可中断"。管道专注数据变换与并行,责任链专注职责传递。理解"管道可并行、责任链可中断"是关键差异,背压控制保证流水线稳定。