Okhttp提供两种请求方式,同步(execute)和异步(enqueue)。同步请求会阻塞当前线程,日常开发基本不会用到。异步请求会创建新线程,管理线程和任务逻辑主要在Dispatcher类里,接下来重点分析这个类,源码版本是3.14.9。
1.线程池
如果要管理线程,一定会用到线程池。我们先重温下线程池的作用
- 复用线程,降低系统资源的使用
- 提高响应速度,减少创建线程的时间
- 管理线程,避免无限创建线程,导致系统崩溃
Dispatcher创建线程池的地方
可以通过构造方法传入自己的线程池,不传的情况就会使用默认的。这个默认的线程池构建不只是类似,和Executors.newCachedThreadPool()是一模一样的,只是改变了threadFactory。
用的是无界队列,最大线程数也是等同于无限。这就涉及一个问题了,怎么避免无限创建线程,毕竟项目中接口的请求是很频繁的,创建线程过多,有可能会导致崩溃。关键就在任务队列的限制。
2.任务队列
除了线程池里面的队列,Dispatcher还另外创建了三个队列
readyAsyncCalls是异步等待队列,runningAsyncCalls是异步执行中的队列,runningSyncCalls是同步执行中的队列。我们先从执行入口enqueue(AsyncCall call)开始分析:
首先会把任务加入等待队列中。findExistingCallWithHost(String host)从readyAsyncCalls和runningAsyncCalls找出和当前任务host服务器地址一样的任务。reuseCallsPerHostFrom(AsyncCall other)复用同一个AtomicInteger来计算每个服务器有多少个任务,后面promoteAndExecute会用到这个callsPerHost会用于判断是否超出服务器最大请求数。
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
asyncCall.callsPerHost().incrementAndGet();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
promoteAndExecute方法的作用是把符合条件的等待任务加入运行队列和线程池执行。判断每个服务器最大请求数前,还判断了所有服务器总请求数限制。
有了maxRequests和maxRequestsPerHost限制,线程池就不会无限创建线程。
任务结束,移出运行队列
finished递减服务器请求数,calls.remove(call)移出运行队列。运行队列有闲置,所以再执行一遍promoteAndExecute(),把等待中的任务允许起来。还会触发回调idleCallback,意图是通知有任务结束,idleCallback默认是空的,需要我们传进去,不过实际作用不大,可以忽略。
调用finished的地方是在Runnable的finally语块里,保证了线程异常也能移出队列。
总结
- 我们可以根据项目需求,设置最大请求数和异步线程池。如果项目的服务器单一,建议把maxRequestsPerHost设置大一些,默认是5。
- Dispatcher应该设置成单例,多个okhttpClient复用同一个Dispatcher,这样maxRequestsPerHost和maxRequests才有意义,也不会创建多个线程池浪费系统资源。