OkHttp 3.7源码分析(三)——任务队列

OkHttp3.7源码分析文章列表如下:

  • OkHttp源码分析——整体架构
  • OkHttp源码分析——拦截器
  • OkHttp源码分析——任务队列
  • OkHttp源码分析——缓存策略
  • OkHttp源码分析——多路复用


前面的博客已经提到过,OkHttp的一个高效之处在于在内部维护了一个线程池,方便高效地执行异步请求。本篇博客将详细介绍OkHttp的任务队列机制。

1. 线程池的优点

OkHttp的任务队列在内部维护了一个线程池用于执行具体的网络请求。而线程池最大的好处在于通过线程复用减少非核心任务的损耗。

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。但如果对多线程应用不当,会增加对单个任务的处理时间。可以举一个简单的例子:

假设在一台服务器完成一项任务的时间为T

T1 创建线程的时间
T2 在线程中执行任务的时间,包括线程间同步所需时间
T3 线程销毁的时间

显然T = T1+T2+T3。注意这是一个极度简化的假设。
可以看出T1,T3是多线程本身的带来的开销(在Java中,通过映射pThead,并进一步通过>SystemCall实现native线程),我们渴望减少T1,T3所用的时间,从而减少T的时间。但一些线>程的使用者并没有注意到这一点,所以在程序中频繁的创建或销毁线程,这导致T1和T3在T中占有>相当比例。显然这是突出了线程的弱点(T1,T3),而不是优点(并发性)。

线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。

  1. 通过对线程进行缓存,减少了创建销毁的时间损失
  2. 通过控制线程数量阀值,减少了当线程过少时带来的CPU闲置(比如说长时间卡在I/O上了)与线程过多时对JVM的内存与线程切换时系统调用的压力

类似的还有Socket连接池、DB连接池、CommonPool(比如Jedis)等技术。

2. OkHttp的任务队列

OkHttp的任务队列主要由两部分组成:

  • 任务分发器dispatcher:负责为任务找到合适的执行线程
  • 网络请求任务线程池
public final class Dispatcher {
  private int maxRequests = 64;
  private int maxRequestsPerHost = 5;
  private Runnable idleCallback;

  /* Executes calls. Created lazily. /
  private ExecutorService executorService;

  /* Ready async calls in the order they'll be run. /
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  /* Running asynchronous calls. Includes canceled calls that haven't finished yet. /
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  /* Running synchronous calls. Includes canceled calls that haven't finished yet. /
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
    this.executorService = executorService;
  }

  public Dispatcher() {
  }

  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

  ...
}

参数说明如下:

  • readyAsyncCalls:待执行异步任务队列
  • runningAsyncCalls:运行中异步任务队列
  • runningSyncCalls:运行中同步任务队列
  • executorService:任务队列线程池:
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
  • int corePoolSize: 最小并发线程数,这里并发同时包括空闲与活动的线程,如果是0的话,空闲一段时间后所有线程将全部被销毁
  • int maximumPoolSize: 最大线程数,当任务进来时可以扩充的线程最大值,当大于了这个值就会根据丢弃处理机制来处理
  • long keepAliveTime: 当线程数大于corePoolSize时,多余的空闲线程的最大存活时间,类似于HTTP中的Keep-alive
  • TimeUnit unit: 时间单位,一般用秒
  • BlockingQueue workQueue: 工作队列,先进先出,可以看出并不像Picasso那样设置优先队列
  • ThreadFactory threadFactory: 单个线程的工厂,可以打Log,设置Daemon(即当JVM退出时,线程自动结束)等

可以看出,在Okhttp中,构建了一个阀值为[0, Integer.MAX_VALUE]的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活60秒,它使用了一个不存储元素的阻塞工作队列,一个叫做"OkHttp Dispatcher"的线程工厂。

也就是说,在实际运行中,当收到10个并发请求时,线程池会创建十个线程,当工作完成后,线程池会在60s后相继关闭所有线程。

3. Dispatcher分发器

dispatcher分发器类似于Ngnix中的反向代理,通过Dispatcher将任务分发到合适的空闲线程,实现非阻塞,高可用,高并发连接

1.同步请求

当我们使用OkHttp进行同步请求时,一般构造如下:

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
    .url("http://publicobject.com/helloworld.txt")
    .build();
Response response = client.newCall(request).execute();

接下来看看RealCall.execute

  @Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    try {
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }

同步调用的执行逻辑是:

  • 将对应任务加入分发器
  • 执行任务
  • 执行完成后通知dispatcher对应任务已完成,对应任务出队

2.异步请求

异步请求一般构造如下:

        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d("OkHttp", "Call Failed:" + e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d("OkHttp", "Call succeeded:" + response.message());
            }
        });

当HttpClient的请求入队时,根据代码,我们可以发现实际上是Dispatcher进行了入队操作。

synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      //添加正在运行的请求
    runningAsyncCalls.add(call);
       //线程池执行请求
    executorService().execute(call);
  } else {
      //添加到缓存队列排队等待
    readyAsyncCalls.add(call);
  }
}

如果满足条件:

  • 当前请求数小于最大请求数(64)
  • 对单一host的请求小于阈值(5)

将该任务插入正在执行任务队列,并执行对应任务。如果不满足则将其放入待执行队列。

接下来看看AsyncCall.execute

@Override protected void execute() {
  boolean signalledCallback = false;
  try {
      //执行耗时IO任务
    Response response = getResponseWithInterceptorChain(forWebSocket);
    if (canceled) {
      signalledCallback = true;
      //回调,注意这里回调是在线程池中,而不是想当然的主线程回调
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
      signalledCallback = true;
      //回调,同上
      responseCallback.onResponse(RealCall.this, response);
    }
  } catch (IOException e) {
    if (signalledCallback) {
      // Do not signal the callback twice!
      logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
      //最关键的代码
    client.dispatcher().finished(this);
  }
}

当任务执行完成后,无论成功与否都会调用dispatcher.finished方法,通知分发器相关任务已结束:

  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }
  • 空闲出多余线程,调用promoteCalls调用待执行的任务
  • 如果当前整个线程池都空闲下来,执行空闲通知回调线程(idleCallback)

接下来看看promoteCalls:

  private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }

      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

promoteCalls的逻辑也很简单:扫描待执行任务队列,将任务放入正在执行任务队列,并执行该任务。

4. 总结

以上就是整个任务队列的实现细节,总结起来有以下几个特点:

  • OkHttp采用Dispatcher技术,类似于Nginx,与线程池配合实现了高并发,低阻塞的运行
  • Okhttp采用Deque作为缓存,按照入队的顺序先进先出
  • OkHttp最出彩的地方就是在try/finally中调用了finished函数,可以主动控制等待队列的移动,而不是采用锁或者wait/notify,极大减少了编码复杂性
时间: 2024-09-28 08:16:49

OkHttp 3.7源码分析(三)——任务队列的相关文章

OkHttp 3.7源码分析(五)——连接池

OkHttp3.7源码分析文章列表如下: OkHttp源码分析--整体架构 OkHttp源码分析--拦截器 OkHttp源码分析--任务队列 OkHttp源码分析--缓存策略 OkHttp源码分析--多路复用 接下来讲下OkHttp的连接池管理,这也是OkHttp的核心部分.通过维护连接池,最大限度重用现有连接,减少网络连接的创建开销,以此提升网络请求效率. 1. 背景 1.1 keep-alive机制 在HTTP1.0中HTTP的请求流程如下: 这种方法的好处是简单,各个请求互不干扰.但在复杂

OkHttp 3.7源码分析(四)——缓存策略

OkHttp3.7源码分析文章列表如下: OkHttp源码分析--整体架构 OkHttp源码分析--拦截器 OkHttp源码分析--任务队列 OkHttp源码分析--缓存策略 OkHttp源码分析--多路复用 合理地利用本地缓存可以有效地减少网络开销,减少响应延迟.HTTP报头也定义了很多与缓存有关的域来控制缓存.今天就来讲讲OkHttp中关于缓存部分的实现细节. 1. HTTP缓存策略 首先来了解下HTTP协议中缓存部分的相关域. 1.1 Expires 超时时间,一般用在服务器的respon

OkHttp 3.7源码分析(一)——整体架构

OkHttp3.7源码分析文章列表如下: OkHttp源码分析--整体架构 OkHttp源码分析--拦截器 OkHttp源码分析--任务队列 OkHttp源码分析--缓存策略 OkHttp源码分析--多路复用 OkHttp是一个处理网络请求的开源项目,是Android端最火热的轻量级框架,由移动支付Square公司贡献用于替代HttpUrlConnection和Apache HttpClient.随着OkHttp的不断成熟,越来越多的Android开发者使用OkHttp作为网络框架. 之所以可以

QEMU1.3.0的源码分析三:user model之linux

从源码目录来看,user model有两块内容bsd-user和linux-user.我主要研究了下linux-user这种情况. 首先要提一下通常容易关注的焦点,linux-user下的函数入口点:/源码目录/linux-user/main.c中的 Line:3388    int main(int argc, char **argv, char **envp). 找到了入口函数,就可以根据这个main函数中的调用关系来看看这个情况下的主要执行流程和动作了. int main(int argc

第二人生的源码分析(三十一)接收数据的流量控制

数据接收回来后,本来就应立即处理掉,这样是比较简单的想法.但由于网络带宽有限,这时就需要限制UDP接收数据的速度.下面就来分析这种需求的实现,它的代码如下: #001 S32 LLPacketRing::receivePacket (S32 socket, char *datap) #002 { #003      S32 packet_size = 0; #004    下面判断是否使用接收的流量限制. #005      // If using the throttle, simulate

定时组件quartz系列&lt;三&gt;quartz调度机制调研及源码分析

quartz2.2.1集群调度机制调研及源码分析引言quartz集群架构调度器实例化调度过程触发器的获取触发trigger:Job执行过程:总结:附:   引言 quratz是目前最为成熟,使用最广泛的java任务调度框架,功能强大配置灵活.在企业应用中占重要地位.quratz在集群环境中的使用方式是每个企业级系统都要考虑的问题.早在2006年,在ITeye上就有一篇关于quratz集群方案的讨论:http://www.iteye.com/topic/40970 ITeye创始人@Robbin在

Backbone源码分析(三)

Backbone源码分析(一)Backbone源码分析(二) Backbone中主要的业务逻辑位于Model和Collection,上一篇介绍了Backbone中的Model,这篇文章中将主要探讨Collection的源码. 让我们先来看一下Collection的构造函数: // Create a new **Collection**, perhaps to contain a specific type of `model`. // If a `comparator` is specified

java io学习(三) 管道的简介,源码分析和示例

管道(PipedOutputStream和PipedInputStream)的简介,源码分析和示例 本章,我们对java 管道进行学习. java 管道介绍 在java中,PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流. 它们的作用是让多线程可以通过管道进行线程间的通讯.在使用管道通信时,必须将PipedOutputStream和PipedInputStream配套使用. 使用管道通信时,大致的流程是:我们在线程A中向PipedOutputStr

HBase源码分析之HRegion上compact流程分析(三)

        在<HBase源码分析之HRegion上compact流程分析(二)>一文中,我们没有讲解真正执行合并的CompactionContext的compact()方法.现在我们来分析下它的具体实现.         首先,CompactionContext表示合并的上下文信息,它只是一个抽象类,其compact()并没有实现,代码如下: /** * Runs the compaction based on current selection. select/forceSelect