android线程池

一,引言

1.遇到的问题

因为项目的特殊性,需要用户在保存数据到本地数据库后,刷新数据时后台同步上传本地数据的数据,为了增加上传图片和数据的效率,使用了线程池管理。

如果数据库操作不会造成主线程的卡顿,那么不用异步线程也行,我这里的数据库数据量太大,已经影响UI卡顿了。

2.处理方案

2.1 AsyncTask.THREAD_POOL_EXECUTOR

查询和数据操作都使用了

AsyncTask.THREAD_POOL_EXECUTOR

来进行数据的查询和异步操作。

2.1.1 为什么使用它

AsyncTask.THREAD_POOL_EXECUTOR是AsyncTask在3.0版本以前并发操作所使用线程池,在3.0以后可以调用++executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)++ 来实现并发调用,用++execute++方法实际上调用的还是++executeOnExecutor++方法,只是使用的是只有一个线程的线程池。

/** @hide */
    public static void setDefaultExecutor(Executor exec) {
        sDefaultExecutor = exec;
    }

在SDK25的源码中,可以看到设置其他线程池为默认线程池的方法已经被标注为隐藏了,所以只能使用默认的串行线程池。

在3.0以后的版本使用AsyncTas的execute方法,都是串行的,当多个execute被执行时,会造成线程等待的效果。如果想要线程立即执行,还得使用executeOnExecutor来实现,线程池可以使用四大线程池如:Executors.newCachedThreadPool(),也可以使用AsyncTask.THREAD_POOL_EXECUTOR。

2.1.2 缺点

但使用AsyncTask.THREAD_POOL_EXECUTOR线程池来实现上传操作确实不可行的。

为了上传效率,上传的每个表,每条数据,每个图片都是使用了新的线程,这将会导致线程池堵塞,使与主线程交互有关的查询等操作耗时过长,影响用户体验。

所以上传的操作还是要自己再创建单独的线程池来进行管理,避免交互线程的堵塞。

2.2 自定义线程池
2.2.1 线程池的创建

线程池的创建一共有6个参数。

  • CorePoolSize 这个参数是核心线程的数量,默认情况下是一直存活的。

如果把allowCoreThreadTimeOut设置为true,那么核心线程也会受keepAliveTime来决定超时时常。

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
  • maximumPoolSize 线程池的最大容量,当活动线程到达这个值之后,后面新增的任务会堵塞,等有空余才添加进去。
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
  • keepAliveTime 非核心线程闲置时的超时时长,超过这个时常非核心线程就会回收。如果设置了++allowCoreThreadTimeOut++为true,核心线程也会被限制。
private static final int KEEP_ALIVE_SECONDS = 30;
  • unit keepAliveTime参数的时间单位,是个枚举。
TimeUnit.SECONDS
  • workQueue 它是堵塞队列,用来实现数据共享。当队列没有数据时,消费者所有线程的自动挂起,直到有数据放入;当队列满时,生产者所有线程都自动挂起,直到队列有空位。++put/take++方法实现了上述功能,而++offer/poll++可以设置时常来实现线程挂起,超过时常返回布尔值。

常用的堵塞队列有ArrayBlockingQueue和LinkedBlockingQueue,他们最大的不用是前者put/take是用的一个锁,放与取无法实现并行;后者放一个锁,取一个锁,可以实现put/take的并行。

private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);
  • RejectedExecutionHandler 拒绝的策略。在使用线程池并且使用有界队列的时候,如果队列满了,任务添加到线程池的时候就会有问题,这时会用到拒绝策略:
  1. AbortPolicy :不执行,会抛出 RejectedExecutionException 异常。默认的策略
  2. CallerRunsPolicy :由调用者(调用线程池的主线程)执行。
  3. DiscardOldestPolicy :抛弃等待队列中最老的。
  4. DiscardPolicy: 不做任何处理,即抛弃当前任务。
  • threadFactory 为线程池中创建新线程,它是个接口,使用时需要实现newThread方法来创建。如果不指定它,将会使用默认的ThreadFactory:DefaultThreadFactory。
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "SynData #" + mCount.getAndIncrement());
        }
    };
2.2.2 线程池的执行逻辑

先说下execute方法:

 public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 1 当前线程数量小于核心线程数据时,创建一个新线程来运行任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2 将任务加入等待队列,当等待队列还有空位插入成功,走入if
    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);
    }
    // 3 当等待队列已满,则创建非核心线程来运行任务
    else if (!addWorker(command, false))
        reject(command);
}

上面是执行execute时,任务的处理,1是创建核心线程运行任务,3是创建非核心线程运行任务。那队列的任务如何运行呢?

addWorker中是创建线程来执行任务,封装在Worker中执行任务。

/**
Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    //实际上线程执行的任务,是下面那个方法的runWorker
    this.thread = getThreadFactory().newThread(this);
}

public void run() {
    runWorker(this);
}

如何执行等待队列里的任务,关键就在runWorker中了,可以看到线程实际都是runWorker方法

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //getTask就是从队列中取出任务,这里循环到等待任务执行完成。
        while (task != null || (task = getTask()) != null) {
           //执行任务,省略
           ...
        }
        completedAbruptly = false;
    } finally {
        //当等待队列中的任务执行完,就移除这个Worker
        processWorkerExit(w, completedAbruptly);
    }
}

上面实现了循环执行等待队列,等待队列执行完就执行processWorkerExit来移除Worker。

等等,核心线程也会被销毁吗? 这就要看++getTask++方法了。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        ...
        //超时策略就在这里了
        //当线程大于corePoolSize或允许核心线程超时时,timed为true
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        //用来退出循环,回收线程,退出循环的条件,下面分析
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        //当timed为true时,根据keepAliveTime来设置线程挂起时间,超过挂起时间则返回空
        //如果timed为false,则挂起线程,等待队列插入数据
        try {
            //通过poll和take方法来实现线程回收
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

可以看出,等待队列的++poll++和++take++方法对于线程回收起到了至关重要的作用。

推测出可回收线程的条件:

  1. 当运行线程大于线程池容量时,一定会回收。
  2. 当运行线程小于线程池容量,大于核心线程数时,根据设置的非核心线程超时时间来获取任务,任务为空则回收。
  3. 当运行线程小于核心线程数大于1。当设置核心线程可回收,且根据超任务获取为空,则回收;核心线程不可回收时则挂起线程。
  4. 当运行线程等于1。设置核心线程可回收,且根据超时取出的任务为空,下次循环如果队列还是为空,则回收;核心线程不可回收则挂起线程。

总的来说,当运行线程小于核心线程数量,根据核心线程是否回收来决定核心线程是否要回收。

至此,对于线程池如何处理任务应该有了一定的了解。

2.3 线程池的一些方法

同步任务不允许并发执行,只能串行,这就需要在执行同步任务时判断同步任务是否正在执行。这里使用THREAD_POOL_EXECUTOR.getActiveCount() == 0来判断。

getActiveCount是用来获取线程池中运行线程的数量。

2.4 使用AtomicInteger

使用AtomicInteger来实现不同线程并发下的线程安全。

2.5 volatile

volatile是个不被推荐使用的变量,因为它的特性:具有 synchronized 的可见性特性,但是不具备原子特性。没有原子特性,无法满足读取-修改-写入操作序列组成的组合操作,使数据在这个过程中无法保持不变。使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。

正是由于其特性,在满足使用场景的情况下,volatile的使用更加简单,性能也更加由于锁。它的读操作几乎和非 volatile一样,写操作为了保证可见性需要实现内存界定使得开销比读大很多,但总开销比起锁还是更低。

差点忘了使用场景:

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

第一条很好理解,就是因为不具备原子特性,写过程不能保证值不变化,如果写要不依赖当前值。第二条也是因为原子特性,当前值如果变化了,不变式就失去了正确性。

总的来说,操作不依赖当前值就可以了。但它比起锁来说太容易出错了。

3.小结

对参数设置如果有一定的了解,对于使用上其实就没什么问题了。

4.参考

ThreadPoolExecutor是如何做到线程重用的

BlockingQueue

线程池的使用(ThreadPoolExecutor详解)

深入理解在Android中线程池的使用

线程池任务统计应用

正确使用 Volatile变量

Java并发集合的实现原理

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

推荐阅读更多精彩内容

  • android对于主线程的响应时间限制的非常严格,稍有不慎就会遇到Application Not Respondi...
    uncle_charlie阅读 904评论 1 13
  • 引言 在Android开发中,只要是耗时的操作都需要开启一个线程来执行。例如网络访问必须放到子线程中执行,否则会抛...
    落叶的位置丶阅读 740评论 0 1
  • 为什么使用线程池 线程是操作系统能进行运算调度的最小单元,在Java 中直接使用线程,给我们带来了很多便利,但是线...
    AnAppleADie阅读 827评论 0 1
  • 注意:本篇文章是本人阅读相关文章所写下的总结,方便以后查阅,所有内容非原创,侵权删。 本篇文章内容来自于:Andr...
    Amy_LuLu__阅读 781评论 0 0
  • 女儿高一!上学期就上了一个月的课,一直在家折腾的!后来我几乎放弃,寒假主动给她钱,让她多出去和初中同学一起玩,她碍...
    徐菲儿阅读 429评论 7 14