Android开发进阶—线程和线程池

来源:互联网 发布:mac粉底液色号选择 编辑:程序博客网 时间:2024/06/06 03:36

1.Android中的线程

       在操作系统中,线程是操作系统能够调度的最小单元,同时线程又是一种受限的系统资源,即线程不能无限制的产生,并且线程的创建和销毁都会有相应的开销。线程在Android开发的过程中是一个很重要的概念,从用途上区分大概可以分为两种主线程和子线程。主线程是指进程所拥有的线程,在Java中默认一个进程只有一个线程,而这个线程就是主线程,主线程又被称为UI线程主要处理界面相关的事情,因为用户会随时和界面进行交互,因此主线程在任何时候都必须保持着较高的响应速度,否则就会产生一种界面卡顿的感觉,甚至主线程阻塞5秒系统就会显示ANR对话框提示用户对应的应用处于无响应状态。而子线程又被称作工作线程,往往用于一些耗时的操作,除了主线程之外的都是子线程。

2.Android中的线程池

       线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。Android中的线程池的概念来源于Java中的Executor,在Java中Executor是一个接口,线程池的真正实现为ThreadPoolExecutor,ThreadPoolExecutor提供了一系列的参数来配置线程池,通过传入不同的参数就可以得到不同作用的线程池。在代码中使用线程池进行优化,可以重用线程池中的线程,避免因为频繁的创建和销毁线程带来的性能上的损耗,同时线程池还能控制程序线程的最大并发数,避免因大量的线程之间相互争抢资源而导致阻塞的现象,线程池还能够对线程进行简单的管理,并提供定时执行以及指定时间间隔进行循环执行等功能。

3.线程池的使用

       Android的线程池主要分为4类,分别为FixedThreadPool、CachedThreadPool、ScheduledThreadPool和SingleThreadExecutor,这4类线程池可以通过Executors所提供的工厂方法来得到,由于Android中的线程池都是直接或者间接的通过配置ThreadPoolExecutor来实现的,因此需要先了解ThreadPoolExecutor。

(1)ThreadPoolExecutor

       ThreadPoolExecutor有四个重载方法,在这里主要介绍参数最多的那个构造方法,这样就可以知道其它方法参数的含义了。

public ThreadPoolExecutor(int corePoolSize,                                 int maximumPoolSize,                                 long keepAliveTime,                                 TimeUnit unit,                                 BlockingQueue<Runnable> workQueue,                                 ThreadFactory threadFactory,                                 RejectedExecutionHandler handler)
       这里的构造方法有7个参数,接下来我们来看看这7个参数的含义:

  • corePoolSize:线程池中核心线程的数量
  • maximumPoolSize:线程池中最大线程数量
  • keepAliveTime:非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长
  • unit:表示keepAliveTime参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等
  • workQueue:线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务,存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的
  • threadFactory:为线程池提供创建新线程的功能,这个我们一般使用默认即可
  • handler:拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException

       对于线程中的任务队列来说,workQueue是一个BlockingQueue的类型,那么这个BlockingQueue又是什么呢?它是一个特殊的队列,当我们从BlockingQueue中取数据时,如果BlockingQueue是空的,则取数据的操作会进入到阻塞状态,当BlockingQueue中有了新数据时,这个取数据的操作又会被重新唤醒。同理,如果BlockingQueue中的数据已经满了,往BlockingQueue中存数据的操作又会进入阻塞状态,直到BlockingQueue中又有新的空间,存数据的操作又会被冲洗唤醒。

       BlockingQueue有多种不同的实现类,主要使用的是以下几个:

  • ArrayBlockingQueue:这个表示一个规定了大小的BlockingQueue,ArrayBlockingQueue的构造函数接受一个int类型的数据,该数据表示BlockingQueue的大小,存储在ArrayBlockingQueue中的元素按照FIFO(先进先出)的方式来进行存取
  • LinkedBlockingQueue:这个表示一个大小不确定的BlockingQueue,在LinkedBlockingQueue的构造方法中可以传一个int类型的数据,这样创建出来的LinkedBlockingQueue是有大小的,也可以不传,不传的话,LinkedBlockingQueue的大小就为Integer.MAX_VALUE
  • PriorityBlockingQueue:这个队列和LinkedBlockingQueue类似,不同的是PriorityBlockingQueue中的元素不是按照FIFO来排序的,而是按照元素的Comparator来决定存取顺序的(这个功能也反映了存入PriorityBlockingQueue中的数据必须实现了Comparator接口)
  • SynchronousQueue:这个是同步Queue,属于线程安全的BlockingQueue的一种,在SynchronousQueue中,生产者线程的插入操作必须要等待消费者线程的移除操作,Synchronous内部没有数据缓存空间,因此我们无法对SynchronousQueue进行读取或者遍历其中的数据,元素只有在你试图取走的时候才有可能存在。我们可以理解为生产者和消费者互相等待,等到对方之后然后再一起离开
       在创建了一个线程并且将线程提交给线程池以后,如果线程池中的线程数未达到核心线程数,则会立马启用一个核心线程去执行,如果线程池中的线程数已经达到核心线程数,且workQueue未满,则将新线程放入workQueue中等待执行,如果线程池中的线程数已经达到核心线程数但未超过非核心线程数,且workQueue已满,则开启一个非核心线程来执行任务,如果线程池中的线程数已经超过非核心线程数,则拒绝执行该任务。
ThreadPoolExecutor threadPoolExecutor =        new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(128));for(int i = 0; i < 30; i++){    final int num = i;    Runnable runnable = new Runnable()    {        public void run()        {            try            {                Thread.sleep(2000);            }            catch (InterruptedException e)            {                e.printStackTrace();            }            Log.d("ThreadPoolExecutor", "Thread:" + num);        }    };    threadPoolExecutor.execute(runnable);}
       上面的代码中使用ThreadPoolExecutor创建了一个线程池管理线程,执行结果如下所示:
       
       从打印出来的Log可以分析出,由于核心线程数为3,workQueue的大小为128,所以我们的线程的执行应该是先启动三个核心线程来执行任务,剩余的27个任务全部存在workQueue中,等待核心线程空余出来之后执行。当然在实际的开发中不会按照上面的例子那样去配置最大线程数和线程队列,如果我们需要自己来配置这些参数,该如何配置呢?这个时候我们可以参考AsyncTask源码中线程池参数的设置情况。
public abstract class AsyncTask<Params, Progress, Result> {    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);
       从上面的代码中可以看出,核心线程数为手机CPU数量+1(cpu数量获取方式为Runtime.getRuntime().availableProcessors()),最大线程数为手机CPU数量×2+1,线程队列的大小为128,在以后使用线程池的过程中可以参考这个再结合自己的实际情况来配置参数。
(2)SingleThreadExecutor
       SingleThreadExecutor是一个核心线程数量固定为1的线程池,它的构造方法如下:
public static ExecutorService newSingleThreadExecutor() {       return new FinalizableDelegatedExecutorService           (new ThreadPoolExecutor(1, 1,                                   0L, TimeUnit.MILLISECONDS,                                   new LinkedBlockingQueue<Runnable>()));  }
        从它的构造方法的源代码中可以看出,核心线程数和最大线程数一样并且都是1,并且线程的超时时间为0,使用SingleThreadExecutor的一个最大好处就是可以避免我们去处理线程同步问题。
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();for(int i = 0; i < 30; i++){    final int num = i;    Runnable runnable = new Runnable()    {        public void run()        {            try            {                Thread.sleep(2000);            }            catch (InterruptedException e)            {                e.printStackTrace();            }            Log.d("SingleThreadPool", "Thread:" + num);        }    };    singleThreadPool.execute(runnable);}
       上面的代码中使用Executors.newSingleThreadExecutor()创建了一个线程池管理线程,执行结果如下所示:
       
       从打印出来的Log可以看出,由于线程池中只有一个核心线程数而没有非核心线程数,所以任务都是一个个按顺序执行,并且都是等上一个任务执行完成才开始执行下一个任务。
(3)FixedThreadPool
       FixedThreadPool是一个核心线程数量固定的线程池,它的构造方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) {           return new ThreadPoolExecutor(nThreads, nThreads,                                         0L, TimeUnit.MILLISECONDS,                                         new LinkedBlockingQueue<Runnable>());      }  

       从它的构造方法的源代码中可以看出,核心数和最大线程数一样并且都是由参数决定,说明在FixedThreadPool中没有非核心线程,所有的线程都是核心线程,且线程的超时时间为0,说明核心线程即使在没有任务可执行的时候也不会被销毁(这样可让FixedThreadPool更快速的响应请求),最后的线程队列是一个LinkedBlockingQueue,但是LinkedBlockingQueue却没有参数,这说明线程队列的大小为Integer.MAX_VALUE(2的31次方减1),OK,看完参数,我们大概也就知道了FixedThreadPool的工作特点了,当所有的核心线程都在执行任务的时候,新的任务只能进入线程队列中进行等待,直到有线程被空闲出来。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);for(int i = 0; i < 30; i++){    final int num = i;    Runnable runnable = new Runnable()    {        public void run()        {            try            {                Thread.sleep(2000);            }            catch (InterruptedException e)            {                e.printStackTrace();            }            Log.d("FixedThreadPool", "Thread:" + num);        }    };    fixedThreadPool.execute(runnable);}
       上面的代码中使用Executors.newFixedThreadPool()创建了一个线程池管理线程,执行结果如下所示:
       
       从打印出来的Log可以看出,执行结果与我们分析的一致,先往核心线程中添加三个任务,剩余任务进入到workQueue中等待,当有空闲的核心线程时就执行任务队列中的任务。
(4)CachedThreadPool
       CachedTreadPool一个最大的优势是它可以根据程序的运行情况自动来调整线程池中的线程数量,它的构造方法如下:
public static ExecutorService newCachedThreadPool() {       return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                     60L, TimeUnit.SECONDS,                                     new SynchronousQueue<Runnable>());  } 
       从它的构造方法的源代码中可以看出,CachedTreadPool中是没有核心线程的,但是它的最大线程数却为Integer.MAX_VALUE,而且它是有线程超时机制的,超时时间为60秒,这里它使用了SynchronousQueue作为线程队列,SynchronousQueue的特点上文已经说过了,这里不再赘述。由于CachedTreadPool的最大线程数为无限大,所以每当我们添加一个新任务进来的时候,如果线程池中有空闲的线程,则由该空闲的线程执行新任务,如果没有空闲线程,则创建新线程来执行任务,通过这样的机制就可以根据程序的运行情况自动来调整线程池中的线程数量。根据CachedThreadPool这样的特点,我们可以在有大量任务请求的时候使用CachedThreadPool,因为当CachedThreadPool中没有新任务的时候,它里边所有的线程都会因为超时而被终止。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();for(int i = 0; i < 30; i++){    final int num = i;    Runnable runnable = new Runnable()    {        public void run()        {            try            {                Thread.sleep(2000);            }            catch (InterruptedException e)            {                e.printStackTrace();            }            Log.d("CachedThreadPool", "Thread:" + num + "——" + Thread.currentThread().getName());        }    };    cachedThreadPool.execute(runnable);    try    {        Thread.sleep(1000);    }    catch (InterruptedException e)    {        e.printStackTrace();    }}
       上面的代码中使用Executors.newCachedThreadPool()创建了一个线程池管理线程,执行结果如下所示:
       
       从打印出来的Log可以看出,每个任务在执行的过程中都先休眠两秒,但是我向线程池添加任务则是每隔1秒添加一个任务,这样的话,添加第一个任务时先开新线程,添加第二个任务时,由于第一个新线程尚未执行完,所以又开一个新线程,添加第三个任务时,第一个线程已经空闲下来了,直接第一个线程来执行第三个任务,依此类推。
(5)ScheduledThreadPool
       ScheduledThreadPool是一个具有定时定期执行任务功能的线程池,它的构造方法如下:
public ScheduledThreadPoolExecutor(int corePoolSize) {       super(corePoolSize, Integer.MAX_VALUE,             DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,             new DelayedWorkQueue());  }  
       从它的构造方法的源代码中可以看出,ScheduledThreadPool的核心线程数是固定的由我们传入的参数决定,最大线程数为Integer.MAX_VALUE,当非核心线程闲置时,则会被立即回收。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);Runnable runnable = new Runnable(){    public void run()    {        Log.d("ScheduledThreadPool", "Thread");    }};scheduledExecutorService.scheduleAtFixedRate(runnable, 1, 1, TimeUnit.SECONDS);
       上面的代码中使用Executors.newScheduledThreadPool()创建了一个线程池管理线程,并且使用scheduleAtFixedRate()将线程加入到线程池中,执行结果如下所示:
       
       从打印出来的Log可以看出,延迟1秒之后每隔1秒执行一次新任务。除此之外ScheduledThreadPool还有如下的几个方法添加任务:

  • schedule(Runnable command, long delay, TimeUnit unit):延迟delay时长启动一次任务
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延迟initialDelay时长后每个period时长执行一次任务
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):第一次延迟initialDelay时长,以后每次延迟delay时长执行一次任务
4.总结
       线程池除了有提交任务的方法之外还提供了其它的方法,对线程池进行操作
  • shutDown() 关闭线程池,不影响已经提交的任务
  • shutDownNow() 关闭线程池,并尝试去终止正在执行的线程
  • allowCoreThreadTimeOut(boolean value) 允许核心线程闲置超时时被回收
  • submit 一般情况下我们使用execute来提交任务,但是有时候可能也会用到submit,使用submit的好处是submit有返回值。使用submit时我们可以通过实现Callable接口来实现异步任务,在call方法中执行异步任务,返回值即为该任务的返回值。




               以上Demo的源代码地址:点击打开链接

          






0 0
原创粉丝点击