OKHttp3源码分析<CacheThreadPool线程池异步请求任务的执行>

本文详细介绍了OkHttp3中线程池的配置及工作原理,包括如何通过线程池提升程序性能,以及线程复用的具体实现过程。

1.线程池的好处

几乎所有需要异步操作或者并发执行任务的程序都可以使用线程池,三个好处:

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
  • 提高响应速度:当任务到达的时候,任务可以不需要等到现成的额创建就能立即执行;
  • 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统稳定性,使线程池可以进行统一的分配、调优和监控。

2.Okhttp3 线程池的配置

实际上是一个CacheThreadPool(核心线程为0,最大线程为Integer.MAX_VALUE),空闲线程等待60秒之后如果没有新任务的话,就终止进程。
适用于执行很多短期异步任务的小程序,或者负载比较轻的服务器.下面的代码就说明了:
Dispatcher.java

  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;
  }

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

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

我们在使用OKhttp3执行 异步请求任务
a)通过OkHttpClient对象实例调用newCall方法

 OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                System.out.println("请求失败");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                System.out.println("请求成功");
                System.out.println(response.body().string());
            }
        });

b).client.newCall(request)返回的是一个RealCall对象:
OkHttpClient.java

  /**
   * Prepares the {@code request} to be executed at some point in the future.
   */
  @Override public Call newCall(Request request) {
    return new RealCall(this, request, false /* for web socket */);
  }

c).RealCall对象中的enqueue方法:
RealCall.java

 @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

这里你也许会有疑问,这个方法传递进去了Callback对象,实例化了一个AsyncCall对象(new AsyncCall(responseCallback)),那我们的请求将在哪里执行呢?
原来RealCall的构造函数中将请求对象赋值给了RealCall中的成员变量originalRequest

  RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.forWebSocket = forWebSocket;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  }

然后再来看看AsyncCall类(这个类是RealCall类的内部类,所以这个类将可以得到RealCall的originalRequest ,也就是我们的请求),AsyncCall类继承于NamedRunnable类,NamedRunnable类是实现了Runnable接口的。
看一下NamedRunnable类的定义:

public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}

从代码中我们可以看到NamedRunnable 的子类会在其run()方法去执行execute()方法。
现在再回到AsyncCall(NamedRunnable 的子类)的execute()方法:

@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          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!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

我们在代码中可以看到Response response = getResponseWithInterceptorChain();这行代码,这究竟是什么:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

这里我们终于知道了我们的请求最终是经过线程池的调用线程执行中的run(),然后run()调用execute()方法,接着execute()方法调用getResponseWithInterceptorChain(),最后我们的请求才得以处理。

d).再回到client.dispatcher()返回的是一个dispatcher对象,看看dispatcher对象中的enqueue()方法:
Dispatcher.java

  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

当运行中的异步请求任务队列(runningAsyncCalls)的长度小于线程池的最大请求数(maxRequests),并且对同一个主机的请求数目小于初始化maxRequestsPerHost的时候,将请求加入运行中异步请求队列,并让线程池去执行这个请求

e).看看这行代码:executorService().execute(call);
这行代码会执行我们的请求,不明白的回去看c)
executorService()返回的是一个CacheThreadPool,这个线程池是一个大小无界的线程池,适用于执行很多的短期的异步任务的小程序,或者负载较轻的服务器。
这个线程池使用的阻塞队列是SynchronousQueue,这是一个没有容量的阻塞队列。每一个插入操作(offer)必须等待另一个线程对应的移除操作(poll),反之亦然。下图是CacheThreadPool中任务传递的示意图:
这里写图片描述
看看CacheThreadPool的execute方法的运行示意图:
这里写图片描述

1)首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程
正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行
offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方
法执行完成;否则执行下面的步骤2)。
2)当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行
SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1)将失
败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。
3)在步骤2)中新创建的线程将任务执行完后,会执行
SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线
程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执
行步骤1)),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于
空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

至此,使用OKhttp3发起一次异步请求的流程就走完了!

3.线程池是如何实现复用的

回到2.c)中的代码,

@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          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!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

最后会执行client.dispatcher().finished(this);,进到Dispatcher类中的

/** Used by {@code AsyncCall#run} to signal completion. */
  void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
  }
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();
    }
  }

看到if (promoteCalls) promoteCalls();,进入 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.
    }
  }

发现线程池中线程的复用正是在这里得到了复用,这个方法将readyAsyncCalls中的任务加到了runningAsyncCalls队列中,以复用已经空闲的线程

<think> 我们正在讨论的是OkHttp库(版本4.9.0)的使用方法以及它可能引发的NullPointerException(空指针异常)问题。下面我将分两部分进行说明:第一部分介绍OkHttp的基本使用方法,第二部分分析使用OkHttp时可能遇到NullPointerException的原因及解决方法。 ### 一、OkHttp 4.9.0 基本使用方法 OkHttp是一个高效的HTTP客户端,用于发送HTTP请求和接收响应。以下是使用OkHttp进行GET和POST请求的示例: #### 1. 添加依赖 在Maven项目中,在`pom.xml`中添加: ```xml <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.9.0</version> </dependency> ``` #### 2. 发送GET请求 ```java import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class GetExample { public static void main(String[] args) throws Exception { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://jsonplaceholder.typicode.com/posts/1") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("Unexpected code: " + response); } System.out.println(response.body().string()); } } } ``` #### 3. 发送POST请求 ```java import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class PostExample { public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); public static void main(String[] args) throws Exception { OkHttpClient client = new OkHttpClient(); String json = "{\"title\":\"foo\",\"body\":\"bar\",\"userId\":1}"; RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder() .url("https://jsonplaceholder.typicode.com/posts") .post(body) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("Unexpected code: " + response); } System.out.println(response.body().string()); } } } ``` ### 二、OkHttp与NullPointerException的关联及解决方法 在使用OkHttp时,可能遇到NullPointerException的情况,常见原因如下: #### 1. 未检查响应体(Response Body)是否为空 - **问题描述**:当HTTP响应没有响应体时(如HEAD请求或某些状态码),调用`response.body()`返回`null`,此时调用`response.body().string()`会导致空指针异常。 - **解决方案**:在访问响应体之前检查是否为`null`。 ```java Response response = client.newCall(request).execute(); ResponseBody body = response.body(); if (body != null) { String responseString = body.string(); // 处理响应字符串 } else { // 处理无响应体的情况 } ``` #### 2. 异步回调中未处理空响应体 - **问题描述**:在异步请求的回调中,同样需要注意响应体可能为空。 ```java client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { // 错误:未检查body是否为空 // String result = response.body().string(); // 可能抛NPE // 正确做法 ResponseBody body = response.body(); if (body != null) { String result = body.string(); // 处理结果 } response.close(); // 注意关闭响应 } }); ``` #### 3. 请求构造不完整 - **问题描述**:在构建请求时,如果某些必要参数未设置(如URL),则会抛出空指针异常。 ```java // 错误示例:未设置URL Request request = new Request.Builder().build(); // 调用build()时,若未设置url,会抛出IllegalArgumentException而不是NPE ``` 实际上,OkHttp在构建请求时会进行参数校验,如果缺少URL会抛出`IllegalArgumentException`,但如果在构建过程中传入了`null`值,则可能导致NPE。 #### 4. 使用过程中对象被置空 - **问题描述**:在多线程环境下,如果共享的`OkHttpClient`实例被置空,则调用其方法会抛出空指针异常。 - **解决方案**:确保`OkHttpClient`实例在整个生命周期内有效,推荐使用单例模式。 #### 5. 回调中未正确处理异常 - **问题描述**:在异步回调中,如果出现网络错误,`onFailure`方法会被调用,但如果在`onResponse`中处理响应时出现异常(如空指针),则可能导致应用崩溃。 - **解决方案**:在回调方法内部使用try-catch捕获异常。 ### 总结 使用OkHttp时,空指针异常主要来源于对可能为`null`的对象(如响应体)未进行空检查。务必在使用任何可能为`null`的对象前进行判断。同时,注意OkHttp请求构建和回调处理中的细节,以避免异常发生。 §§相关问题§§ 1. 如何配置OkHttp的连接超时和读取超时? 2. OkHttp如何处理HTTPS请求和证书验证? 3. 在Android应用中使用OkHttp需要注意哪些问题? 4. 如何优化OkHttpClient的性能? </think>### `com.squareup.okhttp3:okhttp:4.9.0` 使用方法及与 `NullPointerException` 的关联分析 #### 一、依赖项基本使用 ##### 1. 添加依赖 在 Maven 或 Gradle 中添加依赖: ```xml <!-- Maven --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.9.0</version> </dependency> ``` ```groovy // Gradle implementation &#39;com.squareup.okhttp3:okhttp:4.9.0&#39; ``` ##### 2. 核心用法示例 ###### (1) 同步 GET 请求 ```java OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://api.example.com/data") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("Unexpected code: " + response); } System.out.println(response.body().string()); // 注意空指针风险 } ``` ###### (2) 异步 POST 请求 ```java RequestBody body = RequestBody.create( "{\"key\":\"value\"}", MediaType.get("application/json; charset=utf-8") ); Request request = new Request.Builder() .url("https://api.example.com/post") .post(body) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { if (responseBody != null) { System.out.println(responseBody.string()); } } } }); ``` #### 二、常见 `NullPointerException` 场景及修复 ##### 1. 未检查响应体空值 ```java // 错误写法(可能抛NPE) String result = response.body().string(); // 正确写法 if (response.body() != null) { String result = response.body().string(); } ``` > 当响应无 body 时(如 204 No Content),`response.body()` 返回 `null`[^1][^4] ##### 2. 空 URL 请求 ```java // 错误写法(导致NPE) Request request = new Request.Builder().build(); // url未设置 // 正确写法 Request request = new Request.Builder() .url("https://valid.url") // 必须设置有效URL .build(); ``` ##### 3. 未初始化客户端 ```java // 错误写法 OkHttpClient client = null; client.newCall(request); // 抛出NPE // 正确写法 OkHttpClient client = new OkHttpClient(); ``` ##### 4. 回调中空对象引用 ```java client.newCall(request).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { // 未检查response是否成功 response.body().string(); // 若响应失败可能NPE } }); // 应添加状态检查 if (response.isSuccessful() && response.body() != null) { // 处理响应 } ``` #### 三、最佳实践建议 1. **空值防御** - 对所有 `response.body()`、`request.url()` 等返回值进行 `null` 检查 - 使用 Optional 包装: ```java Optional.ofNullable(response.body()) .ifPresent(body -> System.out.println(body.string())); ``` 2. **异常处理增强** ```java try (Response response = client.newCall(request).execute()) { ResponseBody body = response.body(); if (body == null) throw new EmptyResponseException(); // 处理逻辑 } catch (NullPointerException e) { logger.error("空指针异常", e); } ``` 3. **配置超时防止阻塞** ```java OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); ``` #### 四、与其他组件的整合 ```mermaid graph LR A[应用程序] --> B(OkHttpClient) B --> C[HTTP请求] B --> D[连接池] C --> E[网络拦截器] D --> F[连接复用] E --> G[日志记录/认证] ``` > 当与 Retrofit 结合使用时,需确保接口方法返回类型正确(如 `Call<ResponseBody>` 而非 `Call<String>`),避免自动解析导致的空指针[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值