多次网络请求的小锅

bug描述

今天有个bug需要我处理,业务逻辑是这样的:

搜索框中的文本,对应一次请求。但是之前的代码逻辑是这样的,在子线程需要执行时进行判断是不是网络请求的结果可以从缓存中获取,如果不行则进行开启线程请求网络。用线程池管理,Executors.newSingleThreadExecutor(),乍眼看没什么问题,但是问题却很大,因为访问网络到结果放入缓存中这个过程是耗时的,但是其他部分的刷新可能导致请求了很多次,也就是执行了很多次submit。所以就相当于缓存的判断是无效的,这锅怎么解?下面我说下我的几次解锅挣扎。

bug解决

1. 将缓存判断放入子线程的run中。

这样解锅的理由是:对于单个线程来说,缓存的判断和网络请求是同步的。看上去好像完美解决问题,但是问题依然存在,因为这次是Executors.newSingleThreadExecutor()导致的,下面分析代码:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
/*
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 * @param maximumPoolSize the maximum number of threads to allow in the
 *        pool
*/
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

参数2,意味着如果短期执行两次submit就会缓存一个线程在请求队列中.所以导致的结果是每次极大程度上发起两次请求。
那么问题来了,可不可以将maximumPoolSize设置成0呢?那我们继续看代码:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

当maximumPoolSize<=0时候抛出参数不合法异常,所以行不通,这个时候我在DiskLruCache中找到了答案,看代码

public final class DiskLruCache implements Closeable {

  /** This cache uses a single background thread to evict entries. */
  final ThreadPoolExecutor executorService =
      new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    ...
}

把corePoolSize设置成0。我试了试,确实可以达到目的,为什么?我们过一遍流程

ThreadPoolExecutor.java

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //当工作的线程个数小于我们设置的corePoolSize时候添加任务
    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);
}
private boolean addWorker(Runnable firstTask, boolean core) {
         //外部循环
         retry:
         for (;;) {
             int c = ctl.get();//获取当前工作线程数量,数量为{c-(-536870912)}
             int rs = runStateOf(c);//若c>=0时,该值才为0,否则该值一直为-536870912
             /*
              *由上面的一些线程池状态常量值可知,running<shutdown<stop<tidying<terminated
              *若rs>=shutdown,则表明线程池处于stop、tidying、terminated三种状态的一种
              *若rs>=shutdown成立,则进行后面判断,
              *1、线程池处于shutdown状态
              *  1.1、firstTask不为null,则返回false,也即是线程池已经处于shutdown状态,还要添加新的线程,被直接驳回(拒绝)
              *  1.2、firstTask为null
              *     1.2.1、此时意味着线程池状态为shutdown状态,且first为null,若阻塞队列为空,则返回false
              *2、线程处于大于shutdown的状态,则直接返回false
             */
             if (rs >= SHUTDOWN &&
                 ! (rs == SHUTDOWN &&
                    firstTask == null &&
                    ! workQueue.isEmpty()))
                 return false;
             /*
              *进入内循环以下两种情况会跳出该内循环,否则一直会循环
              *1、当工作线程数量超过一定阈值,会直接返回false
              *2、添加工作线程成功,即ctl的值进行了加一
             */
             for (;;) {
                 int wc = workerCountOf(c);//获取工作线程的数量
                 //当线程数量>=536870911或者>=corePoolSize或maximumPoolSize的时候,则返回false
                 if (wc >= CAPACITY ||
                     wc >= (core ? corePoolSize : maximumPoolSize))
                     return false;
                 if (compareAndIncrementWorkerCount(c))//使用unsafe的cas操作对ctl.get()的值进行加一
                     break retry;//跳出这个外循环
                 c = ctl.get();  // Re-read ctl
                 if (runStateOf(c) != rs)//当此时的线程池状态和之前的状态不等时
                     continue retry;//继续内循环   
             }
         }
         //若进行到了此步操作,则表明工作线程数量加了1
         boolean workerStarted = false;
         boolean workerAdded = false;
         Worker w = null;
         try {
             w = new Worker(firstTask);
             final Thread t = w.thread;//该w.thread为worker内部新创建的thread
             if (t != null) {
                 final ReentrantLock mainLock = this.mainLock;
                 mainLock.lock();//开启锁
                 try {
                     //获取锁后,再次获取线程池的状态
                     int rs = runStateOf(ctl.get());
                     /*
                      *1、当线程池的状态处于shutdown以上状态,则直接释放锁,不启动线程,且执行addWorkerFailed方法
                          执行该方法的作用是使工作线程数量-1
                     */
                     if (rs < SHUTDOWN ||
                         (rs == SHUTDOWN && firstTask == null)) {
                         if (t.isAlive()) // 创建的线程处于活跃状态,即被启动了,抛出异常
                             throw new IllegalThreadStateException();
                         workers.add(w);//workers是一个set集合
                         int s = workers.size();
                         if (s > largestPoolSize)//largestPoolSize默认为0,作用是记录set集合中的线程数量
                             largestPoolSize = s;
                         workerAdded = true;//改变该值,为了启动线程,且返回一个addWorker执行成功的状态
                     }
                 } finally {
                     mainLock.unlock();//释放锁
                 }
                 if (workerAdded) {
                     t.start();
                     workerStarted = true;
                 }
             }
         } finally {
             if (! workerStarted)
                 addWorkerFailed(w);
         }
         return workerStarted;
     }

因为在addWorker代码中有这么一句wc >= (core ? corePoolSize : maximumPoolSize))成立则返回false,表明core为false时会以maximumPoolSize来当做corePoolSize比较,按照我们写的那种是返回false,则执行reject(command);

final void reject(Runnable command) {
    //handler是我们最初传递进来的
    //private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
    handler.rejectedExecution(command, this);
}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}

首先第一个请求过来了,执行线程池,发现没有线程可用,所以创建了一个线程,这个线程执行网络请求,但是这个时候紧跟着第二次submit过来了,发现这个线程池只能容得下一个线程,并且这个线程正在执行,所以不能创建新的线程进行网络请求。此时就达到了请求一次的目的。我们提交代码的时候需要审批,大牛看到了这种写法很奇怪说是Daemon的,但是我想了想守护线程和这个没关系啊,所以问他,他说DiskLruCache需要不死线程,我这个线程池会凋亡。这个观点我暂时还不理解,但是大牛又是权威,我不敢问希望那天可以理解。有的童鞋说用AsyncTask,我之前看过AsyncTask源码但是很多细节没有注意,这个时候有点懵逼他们说的同步和并行在AsyncTask中的使用。所以我继续看AsyncTask源码,下面就是解释AsyncTask源码的过程:

2.利用AsyncTask解决

AsyncTask.java

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

@MainThread
public static void execute(Runnable runnable) {
    sDefaultExecutor.execute(runnable);
}

AsyncTask.SerialExecutor.java

private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;
    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;
/**
 * An {@link Executor} that can be used to execute tasks in parallel.
 */
public static final Executor THREAD_POOL_EXECUTOR;

static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            sPoolWorkQueue, sThreadFactory);
    threadPoolExecutor.allowCoreThreadTimeOut(true);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

SerialExecutor是个内部类,他将Runnable放入一个队列中,然后对mActive变量进行判断,如果这个变量为空则执行队列中的请求,首先获取队列中的一个请求赋值给mActive此时mActive不为空,并且此段代码上锁不担心多线程问题,继而执行threadPoolExecutor对应的线程池执行execute,此线程池的定义代码如上图。

结论

这样也可以解决上面说的bug,原因是串行,虽然submit了多次也给mTasks中添加了多次runable,但是第一个请求执行完成之后才能执行第二个,此时第二个请求run内部已经做了缓存的判断,所以能达到效果。

那么我们分析一下使用哪一种方法更好呢?我觉得使用第一种,因为创建线程的个数为一,不浪费资源。但是如果使用AsyncTask针对这个小bug的话可能会消耗更多资源。

但是第一种最好捕获异常或者更换策略别问我为什么,这种事情小声点哈哈,我觉得换成DiscardPolicy最好,啥也不执行

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 为什么使用线程池 当我们在使用线程时,如果每次需要一个线程时都去创建一个线程,这样实现起来很简单,但是会有一个问题...
    闽越布衣阅读 4,240评论 10 45
  • 【JAVA 线程】 线程 进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者...
    Rtia阅读 2,702评论 2 20
  • 原文连接 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发...
    zjk_00阅读 613评论 0 1
  • 早早到单位,把本来安排好的日程重新安排,挤出了一小时~~憋着一口怒气驱车前往我管的那个边远法庭~海阳法庭。 ...
    幸福慢慢来阅读 297评论 0 1
  • 秋风秋雨秋夜月, 思绪思念思重慈。 月暗云深压城悲, 置碗投箸少一人。
    十二明月夜阅读 271评论 0 4