我的Android网络框架之旅(二)

来源:互联网 发布:淘宝势力周多久一次 编辑:程序博客网 时间:2024/04/30 08:51

承接上一篇文章,今天我们来探讨并发网络的线程管理。众所周知在网络请求中,高并发的多线程网络请求非常普遍,我们不能因为上一条网络阻塞影响到其他的网络请求,然而过多的线程又会耗尽移动端上有限的CPU资源。如何处理多并发操作上,各家的网络框架多少都有些差异,今天我们就来看一看应该如何选择。

队列的选择方案

网络请求一般都是采用FIFO的方式进行调度,所以采用队列来存储请求任务最合适不过了,在JAVA中比较常用的队列有以下几种
1.ArrayBlockingQueue
2.LinkedBlockingQueue
3.PriorityBlockingQueue

让我们先来普及以下BlockingQueue的特点
BlockingQueue

多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)

接着我们来看看它的三个常用实现子类

  1. ArrayBlockingQueue
    基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象。ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行。

  2. LinkedBlockingQueue
    基于链表的阻塞队列,同ArrayListBlockingQueue类似,在未指定长度的情况下,默认是最大值,这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

  3. PriorityBlockingQueue
    基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

由上可知,传统的队列是线程阻塞的队列,这就意味着当我们的网络端上生产者端或消费者端不平衡的时候,就很容易产生线程阻塞。举个例子,如果用户在短时间内进行了大量的网络操作,则消费者端的执行速度会远远的大于生产者端的速度,如果使用ArrayBlockingQueue的话,执行效率就会卡在同步锁进行pull()和take()操作的上面,这样的线程阻塞是不被接受的。这个时候LinkedBlockingQueue的优势就远远显示出来了,同一个元素在队列中使用分离锁的选择可以让LinkedBlockingQueue能够高效的处理并发数据。
而优先级队列的使用则体现在另一个场景里,假设现在用户进入了一个主页面需要获取动态数据,同时后台提交了更新用户状态的操作,那么如果更新状态被阻塞的话,用户的主界面数据就会迟迟刷新不出来,这样的用户体验就会变得很差。所以在进行网络请求的队列选择上,综合考虑到移动端的并发效率和网络请求的优先级,PriorityBlockingQueue和LinkedBlockingQueue的结合体才是最完美的解决方案。

多线程的选择方案

在资源匮乏的移动端,性能优化是一个瓶颈。说到多线程并发操作,大家第一时间想到的一定是线程池。使用线程池可以减少在创建和销毁线程上所花的时间以及系统资源的开销 。固定数量的线程池可以有效的防止内存资源被过度消耗殆尽,为UI线程争取更多的资源。在Android的源码中,已经有了最佳实践的模板,我也就不在这里多赘述,上AsynckTask的源码!

 private static final String LOG_TAG = "AsyncTask";    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;    private static final int KEEP_ALIVE = 1;    private static final ThreadFactory sThreadFactory = new ThreadFactory() {        private final AtomicInteger mCount = new AtomicInteger(1);        public Thread newThread(Runnable r) {            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());        }    };    private static final BlockingQueue<Runnable> sPoolWorkQueue =            new LinkedBlockingQueue<Runnable>(128);    /**     * An {@link Executor} that can be used to execute tasks in parallel.     */    public static final Executor THREAD_POOL_EXECUTOR            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

高并发操作的场景下使用线程池来管理线程的确是不错的实践方案,并且我本人是这样做的。但是在我们查看Volley的源码时候会发现,Volley其实并没有用到线程池,而是自己维护了一个长度为4的数组进行多线程的管理。

 // Create network dispatchers (and corresponding threads) up to the pool size.        for (int i = 0; i < mDispatchers.length; i++) {            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,                    mCache, mDelivery);            mDispatchers[i] = networkDispatcher;            networkDispatcher.start();        }

我们可以看到,在进行网络请求分配的时候,从队列里取出的网络任务并没有被交给线程池去分配资源,而是被交给一个初始化好的线程数组进行控制,那么networkDispatcher里面到底做了什么呢?

public void run() {        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);        Request request;        while (true) {            try {                // Take a request from the queue.                request = mQueue.take();            } catch (InterruptedException e) {                // We may have been interrupted because it was time to quit.                if (mQuit) {                    return;                }                continue;            }            try {                request.addMarker(network-queue-take);                // If the request was cancelled already, do not perform the                // network request.                if (request.isCanceled()) {                    request.finish(network-discard-cancelled);                    continue;                }                // Tag the request (if API >= 14)                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {                    TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());                }                // Perform the network request.                NetworkResponse networkResponse = mNetwork.performRequest(request);                request.addMarker(network-http-complete);                // If the server returned 304 AND we delivered a response already,                // we're done -- don't deliver a second identical response.                if (networkResponse.notModified && request.hasHadResponseDelivered()) {                    request.finish(not-modified);                    continue;                }                // Parse the response here on the worker thread.                Response<!--?--> response = request.parseNetworkResponse(networkResponse);                request.addMarker(network-parse-complete);                // Write to cache if applicable.                // TODO: Only update cache metadata instead of entire record for 304s.                if (request.shouldCache() && response.cacheEntry != null) {                    mCache.put(request.getCacheKey(), response.cacheEntry);                    request.addMarker(network-cache-written);                }                // Post the response back.                request.markDelivered();                mDelivery.postResponse(request, response);            } catch (VolleyError volleyError) {                parseAndDeliverNetworkError(request, volleyError);            } catch (Exception e) {                VolleyLog.e(e, Unhandled exception %s, e.toString());                mDelivery.postError(request, new VolleyError(e));            }        }}

在上面我们提到过,BlockingQueue 是一个线程阻塞的队列,所以当队列为空时,消费者线程会一直阻塞等待网络任务的提交,所以在 while (true) {}中,request = mQueue.take();这段代码就是一个线程阻塞的操作,mQuit用来控制线程结束释放资源,如果网络请求没有被Cancel,最后会执行mDelivery.postResponse(request, response)进行将结果回调传递到主线程。整个过程中我们都并没有看见ThreadPoolExecutor的身影。为什么要使用数组而不是线程池呢?
我们先来看一看线程池的调度规则ThreadPoolExecutor使用介绍

当一个任务通过execute(Runnable)方法欲添加到线程池时:

  1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

  2. 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。

  3. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

  5. 当线程池中的线程数量大于
    corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

在线程池里,如果线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务,这就意味着当corePoolSize未满时,会出现进程资源被浪费的情况。相比于运行时申请线程资源,初始化时分配线程资源反而可以节省创建开支。所以这样看来,Volley使用一个 mDispatchers[i] 来管理网络请求的多线程,也不乏是一种优化方案。

在这里单开一篇文章讲述了多线程的处理,总结一下在编写网络框架的并发处理时我们需要考虑到的情况有以下三点:
1.网络请求的高并发线程执行效率
2.请求队列的优先级处理和取消队列
3.多线程的资源分配与释放

下一篇我将详细描述如何使用httpUrlConnection进行请求头的封装,数据报文的拼接,带你了解RFC文档下定义的几种常见网络请求协议。

2 0