OkHttp 的 Dispatcher 调度机制

总体思路

Dispatcher 维护三组队列并用一个 ExecutorService 执行异步任务:

  • readyAsyncCalls:已排队、等待调度的异步请求

  • runningAsyncCalls:正在执行的异步请求

  • runningSyncCalls:正在执行的同步请求

    还暴露 maxRequests(默认 64 )与 maxRequestsPerHost(默认 5 )两道闸门控制并发。异步入队后,通过 promoteAndExecute() 把"达标"的请求从 ready 提升到 running 并提交线程池。

关键数据结构与默认线程池

  • 默认线程池:ThreadPoolExecutor(0, Int.MAX_VALUE, 60s, SynchronousQueue)(即 cached-pool 语义)。你也可传入自定义的 ExecutorService。

  • 两个并发阈值:

    • maxRequests:全局并发上限(默认 64)

    • maxRequestsPerHost:单 host 并发上限(默认 5,WebSocket 不计入

      修改任一值都会触发一次 promoteAndExecute() 以尝试推进排队任务。

调度算法(promoteAndExecute())

简化后的流程(注意:执行阶段不持有锁,避免回调用户代码时死锁):

  1. 加锁遍历 readyAsyncCalls:

    • 若 runningAsyncCalls.size >= maxRequests → 停;
    • 若该调用的 callsPerHost >= maxRequestsPerHost → 跳过;
    • 否则把该调用从 ready 移到 running,并 callsPerHost++,放入"可执行列表"。
  2. 释放锁,逐个 executeOn(executorService) 真正提交到线程池。

  3. 返回当前是否还有运行中的调用。

    对应代码逻辑就在 promoteAndExecute() 与 enqueue()/finished() 的配合里。

单 Host 计数是如何做的?

enqueue() 时,会尝试在现有的 runningready寻找同 host 的调用 ,如果找到了,就复用对方的 AtomicInteger 来统计该 host 的并发计数;这样同一 host 的排队与执行共享同一把计数器,能更精准地卡 maxRequestsPerHost。

生命周期与空闲回调

  • 同步调用:Call.execute() 开始时登记到 runningSyncCalls,结束时 finished() 移除并触发一次调度。

  • 异步调用:AsyncCall 结束时 callsPerHost--,从 runningAsyncCalls 移除,再触发调度。

  • 总运行数归零时,若设置了 idleCallback 会被调用(注意:异步是在回调返回后才视为 idle;同步是在 execute() 返回时)。

与线程池的关系(为什么不在池里排队)

OkHttp 的排队在 Dispatcher 内存里做,不是在线程池队列里做:

  • 线程池用 SynchronousQueue ------ 不排队,能执行就直接交给线程,否则新建线程(直到 Dispatcher 的并发阈值到顶)。

  • 真正的"上限与公平"由 maxRequests/PerHost 控制,而不是线程池大小。

HTTP/2 与 WebSocket 的细节

  • maxRequestsPerHost 是按 URL host 名计数(不同域名可能共享 IP,因此该限制不保证按 IP 收敛)。

  • WebSocket 不占用单 host 配额(避免长连接把配额吃满)。

常见坑 & 建议

  • readyAsyncCalls 是内存队列默认无显式上限;若你持续入队且后端挂起,可能看到内存增长(需要你从业务侧限流或调低 maxRequests)。

  • 调参优先级:先评估 服务器承载端上资源 → 再合理设置 maxRequests/maxRequestsPerHost;必要时自定义 ExecutorService 或在业务层加 Semaphore/令牌桶。

实用示例:自定义并发与空闲回调

ini 复制代码
val dispatcher = Dispatcher().apply {
  maxRequests = 32              // 全局并发
  maxRequestsPerHost = 8        // 单 host 并发
  idleCallback = Runnable { println("network idle") }
}

val client = OkHttpClient.Builder()
  .dispatcher(dispatcher)
  .build()

(如果你自己传 executorService,需确保它能跑得动你设置的最大并发。)

相关推荐
顺凡6 小时前
删一个却少俩:Antd Tag 多节点同时消失的原因
前端·javascript·面试
音符犹如代码8 小时前
Java并发List实战:CopyOnWriteArrayList原理与ArrayList常见面试题
java·开发语言·面试·list
蚂小蚁13 小时前
一文吃透:宏任务、微任务、事件循环、浏览器渲染、Vue 批处理与 Node 差异(含性能优化)
前端·面试·架构
uhakadotcom14 小时前
在使用cloudflare workers时,假如有几十个请求,如何去控制并发?
前端·面试·架构
嵌入式学习之旅17 小时前
嵌入式面试1103
面试·职场和发展
蒙奇D索大17 小时前
【算法】递归算法实战:汉诺塔问题详解与代码实现
c语言·考研·算法·面试·改行学it
重铸码农荣光18 小时前
从「[1,2,3].map (parseInt)」踩坑,吃透 JS 数组 map 与包装类核心逻辑
面试·node.js
疯狂踩坑人19 小时前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
程序员爱钓鱼21 小时前
Python编程实战——Python实用工具与库:Numpy基础
后端·python·面试
晨非辰1 天前
【数据结构初阶】--从排序算法原理分析到代码实现操作,参透插入排序的奥秘!
c语言·开发语言·数据结构·c++·算法·面试·排序算法