天天看点

踏破铁鞋无觅处,从AsyncTask学Android线程池

要做到以上多线程是必不可少的。课本会告诉你什么时候开辟一个线程,但是很少说的一个很重要的问题是结束。比如,我现在在Activity里有一个工作需要创建一个线程执行,但是这个Activity在进入后台后不幸遇到系统回收资源被销毁了。但是这个线程还在漫无目的的游走,耗费资源。

如何结束?先创建一个:

以上使用kotlin的lambda表达式简写了创建<code>Runnable</code>对象部分的代码。主旨还是创建了一个<code>Runnable</code>对象,并将其作为参数传入<code>Thread</code>。

如何让一个<code>Thread</code>能够退出呢?这就要在<code>Runnable</code>身上下功夫了。首先添加一个是否停止的标识<code>isCancelled</code>,一旦值为true则停止线程的运行,否则继续。我们这里不讨论<code>Thread#interrupt()</code>这个方法,这个方法诡异的地方太多。

首先要给<code>Runnable</code>“添加一个属性”作为上文的是否停止的标识。直接添加时不可能的,<code>Runnable</code>只是一个interface,不是class。所以要实现这个借口为一个抽象类,这样就可以添加属性了。

这里使用抽象类,是因为<code>run()</code>方法的实现留给使用的时候给出。

<code>Thread.sleep(2000)</code>用来模拟一个费时的任务。开始之前检测是否取消了线程的执行,执行之后在检测。之后的检测是有的时候任务执行之后需要有持久化处理结果或者修改任务完成情况的标识之类的动作,如果已经取消了线程的执行,即使任务执行完成也不持久化结果、不修改完成情况。

最后都检测完成之后如果没有取消线程,则发出任务完成执行的消息。

发出和处理这些消息的<code>Handler</code>的定义:

运行在UI线程的<code>Handler</code>检测从线程发出的消息,如果是<code>THREAD_CANCELLED</code>那就是线程已经取消了,如果是<code>THREAD_FINISHED</code>那就是线程完全运行结束。之后根据message的消息设置<code>TextView</code>的文本内容。

这里使用了两个按钮来启动和停止线程:

上面用到的<code>Runnable</code>是只做一件事的,如果是连续不断的循环很多事的话也可以使用white语句来控制是否一直执行线程的工作。一旦设置为停止线程,则停止线程任务的循环跳出<code>Runnable#run()</code>方法,结束线程。

完整代码放在附录中。

所以,如果你在Activity里开辟了一个线程,在Activity被回收的时候结束线程就可以这么做:

这样就再也不用担心Activity挂了,线程还阴魂不散了。

既然缘起<code>AsyncTask</code>那就肯定需要读者一起了解一下相关的概念。

比起来使用<code>Handler</code>+<code>Thread</code>+<code>Runnable</code>的多线程异步执行模式来说,使用<code>AsyncTask</code>是简单了非常的多的。

先简单了解一下<code>AsyncTask</code>。

<code>AsyncTask</code>是一个抽象泛型类。三个类型Params,Progress,Result分别对应的是输入参数的类型,精度更新使用的类型,最后是返回结果的类型。其中任何一个类型如果你不需要的话,可以使用java.lang.Void代替。

继承<code>AsyncTask</code>给出自己的实现,最少需要实现<code>doInBackground</code>方法。<code>doInBackground</code>方法是在后台线程中运行的。如果要在任务执行之后更新UI线程的话还至少需要给出<code>onPostExecute</code>方法的实现,在这个方法中才可以更新UI。

上述的两个方法已经构成了一个<code>AsyncTask</code>使用的基本单元。在后台线程处理一些任务,并在处理完成之后更新UI。但是如果一个任务比较长,只是在最后更新UI是不够的,还需要不断的提示用户已经完成的进度是多少。这就是需要另外实现<code>onProgressUpdate</code>方法。并在<code>doInBackground</code>方法中调用<code>publishProgress</code>方法发出每个任务的处理进度。

这个<code>AsyncTask</code>总体上就是这样的了:

到这里各位读者应该对<code>AsyncTask</code>已经有一个总体的认识了。后台任务在<code>doInBackground</code>处理,处理过程的百分比使用<code>publishProgress</code>方法通知,并在<code>onProgressUpdate</code>方法中更新UI的百分比。最后任务处理全部完成之后在<code>onPostExecute</code>更新UI,显示全部完成。

怎么取消一个任务的执行呢?这个机本身还上面的线程的取消基本上一样。只是<code>AsyncTask</code>已经提供了足够的属性和方法完成取消的工作。直接调用<code>AsyncTask#cancel</code>方法就可以发出取消的信号,但是是否可以取消还要看这个方法的返回值是什么。如果是true那就是可以,否则任务不可取消(但是不可取消的原因很可能是任务已经执行完了)。

调用<code>cancel</code>方法发出取消信号,并且可以取消的时候。<code>isCancelled()</code>就会返回true。同时<code>onPostExecute</code>这个方法就不会再被调用了。而是<code>onCancelled(object)</code>方法被调用。同样是在<code>doInBackground</code>这个方法执行完之后调用。所以,如果想要在取消任务执行后尽快的调用到<code>onCancelled(object)</code>的话,就需要在<code>onInBackground</code>的时候不断的检查<code>isCancelled()</code>是否返回true。如果返回的是true就跳出方法的执行。

<code>onCancelled()</code>是API level 3的时候加入的。<code>onCancelled(Result result)</code>是API level 11的时候加入的。这个在兼容低版本的时候需要注意。

但是一点需要格外注意:

下面就来看看线程池的概念。顾名思义,线程池就是放线程的池子。把费时费力,或者影响响应用户操作的代码放在另外一个线程执行时常有的事。但是如果无顾忌的开辟线程,却会适得其反,严重的浪费系统资源。于是就有了线程池。线程池就是通过某些机制让线程不要创建那么多,能复用就复用,实在不行就让任务排队等一等。

这个机制在线程池的构造函数里体现的非常明显:

corePoolSize 线程池里闲着也不回收的线程数量。除非<code>allowCoreThreadTimeOut</code>指定可以回收。

** maximumPoolSize** 线程池允许的最大线程数。

** keepAliveTime** 非核心线程(就是如果核心线程数量<code>corePoolSize</code>定义为1的话,第二个就是非核心线程)的超时时间。

unit <code>keepAliveTime</code>的时间单位,毫秒,秒等。

** workQueue** 存放<code>execute(Runnable cmd)</code>方法提交的<code>Runnable</code>任务。

** threadFactory**线程池用来创建新线程用的一个工厂类。

** handler**线程池达到最大线程数,并且任务队列也已经满的时候会拒绝<code>execute(Runnable cmd)</code>方法提交任务。这个时候调用这个handler。

知道以上基本内容以后,就可以探讨线程池管理线程的机制了。概括起来有三点:

如果线程池的线程数量少于<code>corePoolSize</code>的时候,线程池会使用<code>threadFactory</code>这个线程工厂创建新的线程执行<code>Runnable</code>任务。

如果线程池的线程数量大于<code>corePoolSize</code>的时候,线程池会把<code>Runnable</code>任务存放在队列<code>workQueue</code>中。

线程池的线程数量大于<code>corePoolSize</code>,队列<code>workQueue</code>已满,而且小于<code>maximumPoolSize</code>的时候,线程池会创建新的线程执行<code>Runnable</code>任务。否则,任务被拒。

现在回到<code>AsyncTask</code>。被人广为诟病的<code>AsyncTask</code>是他的任务都是顺序执行的。一个AsyncTask的实例只能处理一个任务。但是在<code>AsyncTask</code>后面处理任务的是一个静态的线程池。在看这个线程池<code>SerialExecutor</code>的<code>execute</code>方法实现:

这个线程池<code>SerialExecutor</code>在处理<code>Runnable</code>的传入参数的时候对这个任务进行了重新包装成了一个新的<code>Runnable</code>对象,并且将这个新的对象存入了一个叫做mTasks的队列。这个新的<code>Runnable</code>对象首先执行传入的任务,之后不管有无异常调用<code>scheduleNext</code>方法执行下一个。于是整体的就生成了一个传入的任务都顺序执行的逻辑。

这个线性执行的静态线程池<code>SerialExecutor</code>的实现非常简单。并不涉及到我们前文所说的那么多复杂的内容。在实现上,这个线程池只实现了线程池的最顶层接口<code>Executor</code>。这个接口只有一个方法就是<code>execute(Runnable r)</code>。另外需要强调一点:mTasks的类型<code>ArrayDeque&lt;T&gt;</code>是一个不受大小限制的队列。可以存放任意多的任务。在线程池的讨论中遇到队列就需要看看容量概念。

<code>SerialExecutor</code>只是进行了简单的队列排列。但是在<code>scheduleNext</code>方法的实现上又会用到一个复杂一些的线程池来执行任务的具体执行。这线程池叫做

<code>THREAD_POOL_EXECUTOR</code>。我们来具体看看其实现:

这个线程池的实现非常具有现实价值。虽然稍后介绍的系统提供的几种线程池的实现就够用。但是难免遇到一些需要自定义线程池的情况。详细解析如下:

CORE_POOL_SIZE 线程池的核心线程数量为设备核心数加一。

** MAXIMUM_POOL_SIZE** 线程池的最大线程数量为核心数的两倍加一。

** KEEP_ALIVE** 线程池中非核心线程的超时时间为一秒。

** sPoolWorkQueue ** 线程池存放任务的队列。最大个数为128个。参考上面说的线程池处理机制,会出现任务被拒的情况。排队的线程池<code>SerialExecutor</code>存放任务的队列是可以认为无限长的,但是<code>THREAD_POOL_EXECUTOR</code>的队列最多存放128个任务,加上线程池核心线程的数量,能处理的任务相对有限。出现任务被拒的情况的几率比较大。所以,往<code>AsyncTask</code>里直接添加<code>Runnable</code>对象的时候需要三思。

** sThreadFactory** 线程池用来创建线程的工厂对象。<code>ThreadFactory</code>是一个只有一个方法<code>Thread newThread(Runnable r);</code>的接口。这里在实现的时候给新创建的线程添加了一个原子计数,并把这个计数作为线程名称传递给了线程的构造函数。

到这里,我们就已经很清楚<code>AsyncTask</code>是如何用一个极其简单的线程池<code>SerialExecutor</code>给任务排队的。又是如何使用一个复杂一些的线程池<code>THREAD_POOL_EXECUTOR</code>来处理具体的任务执行的。尤其是线程池<code>THREAD_POOL_EXECUTOR</code>,在我们实际应用一个自定义的线程池的时候在设定线程池核心线程数量,线程池最大线程数量的时候都依据什么?明显就是设备的CPU核心数。线程分别在不同个CPU核心中做并行的处理。核心数多可以同时处理的线程数就相对较多,相反则会比较少一些。如此设置核心线程数量就会平衡并行处理的任务数量和在处理的过程中耗费的系统资源。

为了让开发者省时省力,系统默认的提供了四种可以适应不同应用条件的线程池:

** newFixedThreadPool** 顾名思义,线程数量固定的线程池,且其数量等于参数指定值。这一类型的线程池的核心线程数量和最大线程数量是一样的。存放任务的队列的容量可以被认为无限大。一旦线程池创建的线程数量等* nThreads*参数值的时候,新增的任务将会被存放在任务队列中等待核心线程可用的时候执行。

** newSingleThreadExecutor** <code>newFixedThreadPool</code>的一个特殊情况,当mThreads值为1的时候。

** newCachedThreadPool** 这一类型的线程池中创建的线程都有60秒的超时时间,由于超时时间比较长等于是线程空闲了以后被缓存了60秒。由于核心线程数量为0,所以创建的线程都是非核心线程。也因此超时时间才管用。任务队列<code>SynchronousQueue</code>非常特殊,简单理解就是一个任务都存放不了。而线程池的最大线程数量又设定为<code>Integer.MAX_VALUE</code>,可以认为是无限大。根据线程池处理任务的机制,可以认为有新任务过来就会创建一个线程去处理这个任务,但是如果存在空闲没有超时的线程会优先使用。

** newScheduledThreadPool** 生成一个<code>ScheduledThreadPoolExecutor</code>实例。可以通过其提供的接口方法设定延迟一定的时间执行或者隔一定的时间周期执行。

来一个例子:

这里是上面例子中使用的全部代码。

欢迎加群互相学习,共同进步。QQ群:iOS: 58099570 | Android: 572064792 | Nodejs:329118122 做人要厚道,转载请注明出处!

本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/sunshine-anycall/p/5506154.html,如需转载请自行联系原作者

继续阅读