网络架构-okHttp分发器

738 阅读9分钟

okHttp介绍

okHttp源码地址
由Square公司贡献的一个处理网络请求的开源项目,是目前Android使用最广泛的网络框架。从4.4开始HttpURLConnection的底层实现采用的是okHttp。

okHttp优点

  • 支持Spdy、Http1.X、Http2、Quic以及WebSocket
  • 连接池复用底层TCP(Socket),减少请求延时
  • 无缝的支持GZIP减少数据流量
  • 缓存响应数据减少重复的网络请求
  • 请求失败自动重试主机的其他ip,自动重定向

okHttp基础使用

Implementation "com.squareup.okhttp3:okhttp:$Version"

Q1:如何决定将请求放入ready还是running?

在Dispatcher类中
 synchronized void enqueue(RealCall.AsyncCall call) {
            //1.对正在请求的数量有限制64 
            // 2.同一域名正在请求的个数也是有限制的 默认是5个
            if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
                runningAsyncCalls.add(call);
                executorService().execute(call);
            } else {
                readyAsyncCalls.add(call);
            }
        }

Q2: 从ready移动running的条件是什么?

在RealCall类中
  @Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      client.dispatcher().finished(this);
    }
  }

这里会执行到client.dispatcher().finished(this);

void finished(RealCall call) {
    finished(runningSyncCalls, call, false);
  }

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

runningAsyncCalls中 移除掉这次的call后,会执行promoteCalls()方法

private void promoteCalls() {
   //再判断一次正在请求数限制与ready要有数据
   if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
   if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
   //遍历ready
   for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
     AsyncCall call = i.next();
     //如果拿到的等待请求的host在正在请求的列表中还没达到5个
     if (runningCallsForHost(call) < maxRequestsPerHost) {
       i.remove();
       runningAsyncCalls.add(call);
       executorService().execute(call);
     }

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

 /** Returns the number of running calls that share a host with {@code call}. */
 private int runningCallsForHost(AsyncCall call) {
   int result = 0;
   for (AsyncCall c : runningAsyncCalls) {
     if (c.get().forWebSocket) continue;
     if (c.host().equals(call.host())) result++;
   }
   return result;
 }

Q3: 分发器线程池的工作行为?

okHttp默认线程池executorService在dispatcher类中做了一个初始化(自己新建一个也可以)
  public synchronized ExecutorService executorService() {
   if (executorService == null) {
   //为啥核心线程数是0?是因为如果你再也不会用到网络请求 那这个核心线程存在没有什么意义(极端场景)
     executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
         new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
   }
   return executorService;
 }
 //线程池
 //1.corePoolSize核心线程数
 //2.maximumPoolSize最大线程数 这个数量是包括了核心线程数的数量 如果最大线程数跟核心线程数一样 说明只有核心线程
 //3.keepAliveTime 最大线程数去掉核心线程数后剩余的线程(空闲线程)缓存的时间
 //4.unit缓存时间的参数 一般是秒
 //5.workQueue线程队列 使用的是BlockingQueue<Runnable> ,BlockingQueue<Runnable>是线程阻塞队列,后面有讲到这个地方有三种队列
 //5.threadFactory线程工厂
 public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory) {
       this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            threadFactory, defaultHandler);
   }
   
   //线程工厂 为了给线程指定一个name
  public static ThreadFactory threadFactory(final String name, final boolean daemon) {
   return new ThreadFactory() {
     @Override public Thread newThread(Runnable runnable) {
       Thread result = new Thread(runnable, name);
       result.setDaemon(daemon);
       return result;
     }
   };
 }

延伸 为什么oktthp使用的是SynchronousQueue(是无容量队列) 而不是其他俩种?(此处我是反复看了几遍才理解的)

首先阐述下这三个队列的基本概念

假设向线程池提交任务时,核心线程都被占用的情况下:

ArrayBlockingQueue:基于数组的阻塞队列,初始化需要指定固定大小。

​ 当使用此队列时,向线程池提交任务,会首先加入到等待队列中,当等待队列满了之后,再次提交任务,尝试加入队列就会失败,这时就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。所以最终可能出现后提交的任务先执行,而先提交的任务一直在等待。

LinkedBlockingQueue:基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定。

​ 当指定大小后,行为就和ArrayBlockingQueu一致。而如果未指定大小,则会使用默认的Integer.MAX_VALUE作为队列大小。这时候就会出现线程池的最大线程数参数无用,因为无论如何,向线程池提交任务加入等待队列都会成功。最终意味着所有任务都是在核心线程执行。如果核心线程一直被占,那就一直等待。

SynchronousQueue : 无容量的队列。

​ 使用此队列意味着希望获得最大并发量。因为无论如何,向线程池提交任务,往队列提交任务都会失败。而失败后如果没有空闲的非核心线程,就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。完全没有任何等待,唯一制约它的就是最大线程数的个数。因此一般配合Integer.MAX_VALUE就实现了真正的无等待。

实战例子讲解分析

例子1
private void testThread() {
        ArrayBlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(1);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS
                , blockingQueue);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务1:" + Thread.currentThread());
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务2:" + Thread.currentThread());
            }
        });
    }
    
    
日志
2020-07-10 21:35:16.427 com.xm.wanapp D: 任务2:Thread[pool-5-thread-2,5,main]
2020-07-10 21:35:16.427 com.xm.wanapp D: 任务1:Thread[pool-5-thread-1,5,main]
2020-07-10 21:35:21.734 com.xm.wanapp D: 任务2:Thread[pool-6-thread-2,5,main]
2020-07-10 21:35:21.734 com.xm.wanapp D: 任务1:Thread[pool-6-thread-1,5,main]
2020-07-10 21:35:51.404 com.xm.wanapp D: 任务1:Thread[pool-7-thread-1,5,main]
2020-07-10 21:35:51.405 com.xm.wanapp D: 任务2:Thread[pool-7-thread-2,5,main]

代码分析
我们启动2个任务后 第一个任务会先进入队列中,然后立即执行了executor.execute方法
 public void execute(Runnable command) {
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
会进入addWorker(null, false)方法,按理来说会创建线程先执行任务1,然后把任务2加入到队列中,此时不存在空闲线程且队列容量为1,所以会 等任务1执行完了再执行任务2,因为这个时候是处于上图中所说的当线程数大于核心线程数,不存在空闲线程,新任务会添加到等待队列。然而日志出现了先执行任务2,再执行任务1的场景(这点不太理解,如果后续找到原因再补充),总的来说线程1,线程2会执行。
例子2
 private void testThread() {
        ArrayBlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(1);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS
                , blockingQueue);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务1:" + Thread.currentThread());
                //多了这一步
                while (true){
                }
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务2:" + Thread.currentThread());
            }
        });
    }

日志
2020-07-10 21:55:30.042 com.xm.wanapp D: 任务1:Thread[pool-1-thread-1,5,main]
2020-07-10 21:55:30.042 com.xm.wanapp D: 任务2:Thread[pool-1-thread-2,5,main]

这次结果又超出我的预料,好尴尬,我的认知里按理来说线程1进入队列以后会直接执行,但是因为whiletrue导致,线程2会进入队列中等待空闲线程,但是现在没有空闲线程 那线程2会处于一直等待中,直到线程1执行完,所以按理来说日志应该只会打印线程1的日志,也留到下一次定位到问题再说
例子3
private void testThread() {
        LinkedBlockingDeque<Runnable> blockingQueue = new LinkedBlockingDeque<>(1);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS
                , blockingQueue);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务1:" + Thread.currentThread());
                while (true){

                }
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务2:" + Thread.currentThread());
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务3:" + Thread.currentThread());
            }
        });
    }
日志
2020-07-10 21:59:01.927 ? D: 任务1:Thread[pool-1-thread-1,5,main]
2020-07-10 21:59:01.927 ? D: 任务2:Thread[pool-1-thread-2,5,main]
2020-07-10 21:59:01.927 ? D: 任务3:Thread[pool-1-thread-2,5,main]
此处跟上个例子不同的是使用的是LinkedBlockingDeque且容量设置为1,算了,我已经放弃挣扎了,看了这个日志,我已经无法诡辩了,这还是打印了线程2跟线程3的日志,并没有被线程1卡住,后续吧
例子4
private void testThread() {
        LinkedBlockingDeque<Runnable> blockingQueue = new LinkedBlockingDeque<>();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS
                , blockingQueue);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务1:" + Thread.currentThread());
                while (true){

                }
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务2:" + Thread.currentThread());
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务3:" + Thread.currentThread());
            }
        });
    }
日志
2020-07-10 22:04:37.139 com.xm.wanapp D: 任务1:Thread[pool-1-thread-1,5,main]
这次日志其实才符合我的预期,因为虽然我容量值为Integer.MAX_VALUE,但是因为没有空闲线程,所以只会执行线程1
例子5
private void testThread() {
        SynchronousQueue<Runnable> blockingQueue = new SynchronousQueue<>();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS
                , blockingQueue);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务1:" + Thread.currentThread());
                while (true){

                }
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务2:" + Thread.currentThread());
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务3:" + Thread.currentThread());
            }
        });
    }
日志
2020-07-10 22:06:39.261 com.xm.wanapp D: 任务1:Thread[pool-1-thread-1,5,main]
2020-07-10 22:06:39.261 com.xm.wanapp D: 任务3:Thread[pool-1-thread-3,5,main]
2020-07-10 22:06:39.262 com.xm.wanapp D: 任务2:Thread[pool-1-thread-2,5,main]
这次其实才是我想要的,因为SynchronousQueue的容器默认为0且没有空余线程,所以线程2、3添加到任务队列会失败,又因为线程数量小于最大线程数,因此会新建线程执行新任务,不会出现卡死的现象,这就是为啥使用SynchronousQueue
延伸例子6看看执行的先后顺序
 private void testThread() {
        ArrayBlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(1);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS
                , blockingQueue);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务1:" + Thread.currentThread());
                try {
                    Thread.sleep(1_1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务2:" + Thread.currentThread());
            }
        });
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"任务3:" + Thread.currentThread());
            }
        });
    }
日志
2020-07-10 22:12:55.803 com.xm.wanapp D: 任务1:Thread[pool-1-thread-1,5,main]
2020-07-10 22:12:55.808 com.xm.wanapp D: 任务3:Thread[pool-1-thread-3,5,main]
2020-07-10 22:12:55.820 com.xm.wanapp D: 任务2:Thread[pool-1-thread-2,5,main]
分析:执行顺序是1,3,2 部分符合预期 因为1先执行但是有个延时,导致2在队列中等待,当执行到任务3的时候添加到任务队列中失败,那就会新建线程2先执行任务3,等待任务3执行完了 任务1还在执行 那就会使用线程2执行任务2,按理来说只会有线程1跟2 但是实际日志中出现了任务3 不太理解

用到的知识:
1.双端队列