天天看点

android中线程的总结前言AsyncTaskHandlerThreadIntentServiceThreadPoolExecutor总结参考文献

android

thread

线程

前言

开发安卓几年了,由于工作比较忙平常也没太整理基础知识,现在觉得基础知识需要系统的复习。今天就把安卓的线程复习一下。 

安卓常用到的线程有哪些呢?

  • Thread:基本线程,没啥说的
  • AsyncTask:这个类可以说是Handler+Thread的组合。我们平常用Handler可以解决些简单的线程间通信问题,但是当遇到复杂点需求时,用Handler写代码就太臃肿了,AsyncTask帮我们做了封装,让代码写起来更优雅,使用更方便。
  • HandlerThread:这个类其实就是一个线程,它继承了Thread,但是在它的方法里,创建了一个looper,就是说在HandlerThread线程里创建了一个消息队列来处理事情。
  • IntentService:这个类继承了Service,所以它是一个服务,但它里边实现用的是HandlerThread,那既然有了HandlerThread为啥还要出来个IntentService啊,因为Thread的优先级是比Service的优先级低的,就是说我们创建一个HandlerThread线程比IntentService线程更容易被系统kill掉。
  • java工具类线程池:ThreadPoolExecutor,这个就是java为我们实现的线程池类。

AsyncTask

  • 虽然说它是Handler+Thread的组合,但是并不是简单的Thread,而是线程池。
  • 因为有Handler的存在,它跟hanler一样必须在主线程创建,虽然是线程池的构造,但是它默认是串行执行任务的,而不是并发执行。为什么这样设计呢,因为设计者考虑到我们这些菜鸟在调用AsyncTask时候,这块调用一次,那块调用一次,可能不小心会两个task都调用这块共享资源,这就可能造成并发问题,而我们这些菜鸟并不太了解Asynctask,所以没有处理并发的意识,造成程序由于并发而产生意外的结果。所以默认是串行执行任务的。当然如果你不是菜鸟可以修改它的默认设置,改成并发执行。
  • 由于串行执行任务的设计,假如一个task执行一个任务耗时超过5秒,后边任务还处于等待,所以使用AsyncTask执行的任务,尽量不要超过5秒(官方建议)。
  • 因为AsyncTask跟Handler类似,所以也会出现内存泄露问题,我们需要使用static+Weakreference方案来防止内存泄露。

HandlerThread

HandlerThread其实本身就是一个Thread,跟普通Thread的不同之处在于,它内部维护了一个loop队列。

对于这种设计我们在日常中有什么应用呢?

就是一些频率高的耗时处理,用HandlerThread做再合适不过了。

举个例子,比如人脸识别操作,相机一直在高效的采集图像,每秒钟需要处理10张图片做对比,我们只需要把最后的结果交给UI线程更新就行了,至于高频的对比操作过程,交给HandlerThread来做。这样可以避免主线程的卡顿。

当然我们还是要注意,HandlerThread的线程Priority不是太高,执行耗时操作容易被kill,我们可以在构造方法里设置它的优先级,这样结合自己的实际业务来设置对应的级别,毕竟虚拟机只允许少量的高级别线程工作。

IntentService

大家都清楚,在Android的开发中,凡是遇到耗时的操作尽可能的会交给Service去做,比如我们上传多张图,上传的过程用户可能将应用置于后台,然后干别的去了,我们的Activity就很可能会被杀死,所以可以考虑将上传操作交给Service去做,如果担心Service被杀,还能通过设置

startForeground(int, Notification)

方法提升其优先级。

那么,在Service里面我们肯定不能直接进行耗时操作,一般都需要去开启子线程去做一些事情,自己去管理Service的生命周期以及子线程并非是个优雅的做法;好在Android给我们提供了一个类,叫做

IntentService

上边是引用鸿洋大神博客IntentService的开篇介绍详细。

说的很清楚,也是我上边提到的,为了提升优先级防止被kill,我们将HandlerThread提升到IntentService。

ThreadPoolExecutor

当需要处理耗时操作时,我们希望线程并行处理来提高效率,用线程池来处理是在合适不过了。

其实这块我们应该注意一点,何时使用多线程,我觉得下边这个例子说的很恰当:

举个例子,你要做饭,你要做的饭是米饭和一个炒菜。

如果是单线程,那么你可以如下做:

第一种方法:先炒菜,然后开始蒸米饭;

第二种方法:先蒸米饭,等米饭熟了再炒菜;

如果是多线程,那么你就可以如下做:

先蒸米饭,在蒸米饭的过程中去炒菜。

有些问题的解决用多线程会提高效率,比如上边的例子。但是有时不会提高效率,反而会影响效率:

比如,你要洗衣服,还打算做家庭作业(假设你是小学生,老师给你布置的家庭作业)。

如果是单线程:你要么洗完衣服做作业,要么做完作业洗衣服。

如果是多线程:你洗一分钟衣服做一分钟作业,交叉进行,显然有些时间都耗在了任务的切换上了。

所以,多线程主要用于,当一个任务需要不占用资源的等待的时候,可以使用空闲的资源做其他的事情。比如类似于QQ聊天的程序,程序的一个线程一直在等待着看是否有好友发消息过来,而与此同时另一个线程允许你打字并且将自己的消息发送给对方。

以上例子并不是很完美,只是希望能借这些例子对多线程有所理解。

就是说当有2个或2个以上任务的时候,每个任务中间都有等待时间,这个等待时间如果用单线程,是完全的浪费资源,如果用多线程,能充分的利用等待时间来做其他事情。在我们的日常工作中,有这种等待时间的操作,一般是IO操作,网络,图片处理等。所以这方面需要用到多线程,来弥补等待的时间。

何时用线程池呢?我觉得是任务比较多的情况下,用线程池来处理再合适不过了。举个例子:

当你现有有个需求,需要处理100个任务。考虑到机器的效能,我们只能最多让5个线程同时执行任务。那如何让5个线程来执行100个任务呢?因为每个线程只能对应一个任务来执行,所以执行的逻辑保证是先分配5个线程5个任务,然后就是哪个线程先执行完了,接着执行下一个任务。如果让我们来分配这个任务。是不是感觉有点麻烦?如果利用线程池,这些任务我们就不用操心了。直接创建newFixedThreadPool(5),遍历100个任务,执行execute方法100次即可。省去了我们自己去维护5个线程,给5个线程分配任务的代码。

说道java的线程池有些类我们总是模糊,Executor,Executors,ThreadPoolExecutor,ExecutorService,方法newFixedThreadPool,newFixedThreadPool,newSingleThreadExecutor等。 

  • Executor是线程池的最顶端,ThreadPoolExecutor和ExecutorService就是实现的这个接口。接口里就一个方法execute。 
  • Executors相当于一个工具类,它的里边都是静态方法,像newFixedThreadPool,newFixedThreadPool,newSingleThreadExecutor都是这个类的静态方法。 
  • ThreadPoolExecutor是java实现的线程池类,像newFixedThreadPool,newFixedThreadPool,newSingleThreadExecutor这些方法其实都是new的ThreadPoolExecutor实例,只是传的参数不一样而已。

所以说,在上边的简单介绍可以知道,我们主要了解ThreadPoolExecutor就行了,说到底都是new的这个类对象。

合理的使用线程池能够带来3个很明显的好处:(参考)

1.降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗

2.提高响应速度:任务到达时不需要等待线程创建就可以立即执行。

3.提高线程的可管理性:线程池可以统一管理、分配、调优和监控。

构造方法的参数,我们还是有必要了解一下的(参考):

  • corePoolSize

    核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。

  • maximumPoolSize

    线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。

  • keepAliveTime

    非核心线程的闲置超时时间,超过这个时间就会被回收。

  • unit

    指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。

  • workQueue

    线程池中的任务队列BlockingQueue.常用的有三种队,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue;

  • threadFactory

    线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法

    通过线程工厂可以对线程的一些属性进行定制。

    如果不传入这个值,系统会默认使用Executors.defaultThreadFactory()。

  • RejectedExecutionHandler

    RejectedExecutionHandler也是一个接口,只有一个方法

    void rejectedExecution(Runnable var1, ThreadPoolExecutor var2);

    当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution方法。

线程池中的几个状态还是需要知道的:(参考)

  1. RUNNING:可以新增任务, 同时可以处理队列中的任务;
  2. SHUTDOWN:不能新增任务,但是可以处理队列中的任务;状态切换:由RUNNING到SHUTDOWN通过shutdown方法来切换。
  3. STOP:不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。状态切换:由(RUNNING或者SHUTDOWN)到STOP通过shutdownnow方法来切换。
  4. TIDIING:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN 到TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
  5. TERMINATED:线程池彻底终止,就变成TERMINATED状态。状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

分析ThreadPoolExecutor类我们可以从execute方法开始分析。我将execute方法做了注释,如下:

/**
     * Executes the given task sometime in the future.  The task
     * may execute in a new thread or in an existing pooled thread.
     *
     * If the task cannot be submitted for execution, either because this
     * executor has been shutdown or because its capacity has been reached,
     * the task is handled by the current {@code RejectedExecutionHandler}.
     *
     * @param command the task to execute
     * @throws RejectedExecutionException at discretion of
     *         {@code RejectedExecutionHandler}, if the task
     *         cannot be accepted for execution
     * @throws NullPointerException if {@code command} is null

	 翻译:在将来的某个时刻执行所给的任务,任务可能在新创建的线程里执行也可能
	 在一个已经存在线程池中的线程执行,如果一个任务由于一些原因不能提交到线程池,
	 或者因为executor已经关闭了,又或者线程池的容量已经满了,任务就会被构造方法
	 里传入的RejectedExecutionHandler对象拒绝。
     */
	 
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.

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

这个流程基本能简单的明白线程池的创建线程原理,可以概括为:

1.如果当前工作线程数,少于核心线程数,对于新添加进来的任务,会通过新建线程来执行这个任务。

2.如果当前工作线程数大于等于核心线程数,则会将任务添加到队列中,如果向任务队列添加不成功的话,跳入下个判断。

3.如果向任务队列添加任务不成功的话,则尝试新建线程来执行任务,如果新建线程不成功的,抛出异常。

当然还有细节需要分析,比如addWorker里的源码,这里就不展开写了。

Executors中的几个线程池:

FixedThreadPool 构造方法传入一个int值,该值代表核心线程数和工作线程数一样,非核心线程消亡时间是0秒,使用的队列是linkedBlockingQueue.这个线程池的特点是,你int值传入是多少,就会创建多少个线程,然后工作的话,就是这几个线程来按照任务的先后顺序依次执行。

SingledThreadPool,这个就是上边线程的特殊情况,相当于int值是1的情况。这种使用场景是当我们需要一个线程,来处理多个任务时,这些任务会按照队列的形式排好顺序。等待线程执行。

CacheThreaPool,这个线程池的特点是多少个任务就会分配多少个线程。线程和任务是一一对应的关系。这个要主要,任务太多的话,系统会崩溃。使用场景就是任务数量有限,而且需要每个任务对应不同线程的情况。

DelayThreaPool,延时线程,就是按照固定时间间隔来执行任务

在ThreadPoolExecutor的构造方法里我们看到有传入队列BlockingQueue(阻塞队列),阻塞队列也是一个很重要的概念。

BlockingQueue是一个接口,里边定义了许多方法,下边列一些核心方法:

放入数据:

  offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,

    则返回true,否则返回false.(本方法不阻塞当前执行方法的线程)

  offer(E o, long timeout, TimeUnit unit),可以设定等待的时间,如果在指定的时间内,还不能往队列中

    加入BlockingQueue,则返回失败。

  put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断

    直到BlockingQueue里面有空间再继续.

获取数据:

  poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,

    取不到时返回null;

  poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,

    队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。

  take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到

    BlockingQueue有新的数据被加入; 

  drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数), 

    通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

java.util.current包里为我们准备了几种阻塞队列都是实现的BlockingQueue,以下内容参考

1. ArrayBlockingQueue

      基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

  ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

2. LinkedBlockingQueue

      基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。

3. DelayQueue

      DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

使用场景:

  DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。

4. PriorityBlockingQueue

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

5. SynchronousQueue

      一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。

  声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:

  如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;

  但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

当我们了解了这几个队列后,我们看下在Executors方法里都用的哪个队列。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
           

我们常用的newFixedThread方法用到的队列是LinkedBlockingQueue.

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
           

我们常用的newCachedThreadPool方法用到的队列是SynchronousQueue。

总结

以上呢,只是这几个线程的思想概述,具体的用法,还有一些细节的东西,我们还是要深入理解和学习的。针对每一个类的详细介绍这块就不写了,网上有大把现成的文章。

参考文献

Developers 

java多线程以及Android多线程 

你真的了解AsyncTask