本文共 5862 字,大约阅读时间需要 19 分钟。
前言:上一篇博文说到了线程池和AsyncTask的联系和基本的使用。也提到了AsyncTask的底层实现实际上是封装了2个线程池(一个用于排队,一个用于执行任务)和一个Handler,这两个线程池具体是怎么工作的呢?还有提到了AsyncTask并不适合执行特别耗时的任务,若是执行这种任务又会怎么样呢?接下来走进源码,继续剖析AsyncTask的工作原理。
public static final Executor SERIAL_EXECUTOR =new SerialExecutor();
private static volatile Executor sDefaultExecutor =SERIAL_EXECUTOR;
private static class SerialExecutorimplements Executor {
//定义了一个双端队列,用于存储任务队列
final ArrayDeque<Runnable>mTasks =new ArrayDeque<Runnable>();
Runnable mActive;
//execute()方法的实现是将每一个提交的任务都添加到双端队列中(由此可见这个线程池的作用是排队任务)
//并且每次添加都会在finally语句中调用scheduleNext()使用THREAD_POOL_EXECUTOR(另一个线程池)来执行任务
public synchronized void execute(final Runnabler) {
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);
}
}
}
分析:从上面可以看出SERIAL_EXECUTOR内部利用一个ArrayDeque双端队列对每次从UI线程中调用execute()传递进来的任务进行排队,之后会调用scheduleNext()方法,使用另一个线程池执行任务。这样做是不会导致任何问题的,因为注意到scheduleNext()方法是一个被synchronized修饰的方法,也就是不能并行访问,只要正在被调用,那么其他的任务只能在阻塞队列上排队等待。等到这个方法被“解锁”释放,下一个任务才能够行。
b.THREAD_POOL_EXECUTOR,这是一个真正执行任务的方法
THREAD_POOL_EXECUTOR的源码定义如下(关于线程池的配置参数可看上一篇博文):
private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactorysThreadFactory =new ThreadFactory() {
private final AtomicIntegermCount =new AtomicInteger(1);
public Thread newThread(Runnabler) {
return new Thread(r,"AsyncTask #" +mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable>sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(10);
public static final ExecutorTHREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE,
TimeUnit.SECONDS,sPoolWorkQueue,sThreadFactory);
分析:可以看到,这个线程池最大的线程数量是128,核心线程数量是5,队列长度是10,这里要注意一下这个队列和上面提到的ArrayDeque有着本质差别。前者是线程池中的任务队列,而后者是待进入线程池的队列。
public final AsyncTask<Params, Progress, Result> execute(Params...params) {
return executeOnExecutor(sDefaultExecutor,params);
}
//直接传入一个Runnable任务
public static void execute(Runnablerunnable) {
sDefaultExecutor.execute(runnable);
}
分析:这就比较有趣了,先看第二个版本的重载,明显可以看出它调用的是用于排队的线程池(sDefaultExecutor)的execute()方法,这样会使得所有提交的任务都会在进入线程池之前通过ArrayDeque进行排队等候,并且通过同步方法scheduleNext()对THREAD_POOL_EXECUTOR这个真正执行任务线程池进行互斥地访问,也就是说。调用这个重载版本执行任务的话,任务是串行进入线程池执行的,并不存在并发的情况;
再看第二个版本,也是同理的,它调用了executeOnExecutor()方法(下面会对这个方法进行介绍,这里先不理会),将用于任务排队的线程池作为参数传递进去,也就是也会采用上面提到的方式进行串行任务执行。
小结:若是采用execute()方法来使用AsyncTask执行异步任务的话,默认是采用的串行的任务执行方式,每次进入线程池的只能有一个任务!(这里说的线程池自然是THREAD_POOL_EXECUTOR用于执行任务的线程池)。那么我们很容易可以知道,这种串行执行任务的方式,不会产生复杂的同步问题。并且!这种方式对任务的数量也是没有限制的(只受限于内存),因为虽然说THREAD_POOL_EXECUTOR只有128个线程和10个位置的任务队列,但是我们每次进入该线程池的不过是只有一个!其他的任务都在ArrayDeque上进行排队等候进THREAD_POOL_EXECUTOR执行,因此数量上不会受限于线程池THREAD_POOL_EXECUTOR的大小。
=====================分割线(实验一)=========================
口说无凭,接下来进行验证:
//这里我们将每个模拟任务时长定义为10s,这是为了顺便验证是否AsyncTask不可以执行特别耗时的任务(源码中声明:一般超过几s就视为特别耗时的任务)
//源码如下:
public class MyAsyncTaskextends AsyncTask<Void, Void, Integer>{
private int id;
public MyAsyncTask(int id) {
this.id =id;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Integer doInBackground(Void...params) {
try {
Thread.sleep(10000);
}catch (InterruptedException e) {
e.printStackTrace();
}
return id;
}
@Override
protected void onProgressUpdate(Void...values) {
super.onProgressUpdate(values);
}
//输出标识
@Override
protected void onPostExecute(Integerresult) {
Log.i("MyAsyncTask:",id + "");
}
}
//在UI线程中启动AsyncTask:
btn_start.setOnClickListener(new OnClickListener() {
@Override
public void onClick(Viewv) {
for (int i = 0; i < 1000;i++) {
new MyAsyncTask(i).execute();
}
}
});
//结果如下:
//结果分析:
可以看到,时间上每隔10s执行完一次任务,并且异步任务的顺序也没有乱,可以得知,调用execute()执行异步任务的确是串行执行的。并且还可以知道其实是可以执行特别耗时的任务的!要是觉得10s,可以加多时间试试看,也是一样的。
==========================分割线(实验一)========================
b.上面讨论了默认的执行方式是串行的,那么AsyncTask如何并发执行任务呢?要知道,在串行执行的情况下,每次进入THREAD_POOL_EXECUTOR的只有一个任务,是远远不能很好的利用该线程池的资源的!通过观察上面execute()方法的第一个重载版本,我们不难发现它其实调用了executeOnExecutor()方法的。查看源码该方法签名如下:
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executorexec,Params...params)
也正是execute()的第一个重载版本默认将sDefaultExecutor作为参数传递进去,才导致了任务排队并且串行执行。那么我们是否可以直接调用executeOnExecutor()并将THREAD_POOL_EXECUTOR作为参数传递使之变成并行呢?答案是肯定的。验证如下:
============================分割线(实验二)===============================
//MyAsyncTask的代码不变,启动的任务数量改为128+10(也就是刚好是THREAD_POOL_EXECUTOR的最大线程数量+可排队任务的数量)。按照上一篇介绍过的,任务到来时候先会按需要开启核心线程,直到开满5个(corePoolSize)。接着到来的任务开始在队列上排队,直到排满10个(AsyncTask中的声明)。接着到来的任务,由于队列已满,会开启非核心线程,直至总线程数量未128个。再多来的任务就会拒绝了。UI线程中代码如下:
btn_start.setOnClickListener(new OnClickListener() {
@Override
public void onClick(Viewv) {
for (int i = 0;i < 128 + 10; i++) {
new MyAsyncTask(i).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
});
//结果如下:
//结果分析:
可以发现,首先速度真的快了很多很多。。其次发现任务的顺序不再是串行的,而是并行的!这也说明必须要处理适当的同步问题。还有是不会溢出,向上面分析过,128+10应该是它能承受的最多的任务数量了(当然你要保证单个任务时间足够久,否则可能迅速地执行完继续插入队列或是执行,总量依旧可能维持在128+10之下)。//现在试试将任务调为128+11,看看是否会拒绝任务?
好的,不负众望,拒绝了我的任务。。。并行执行得证!
=========================分割线(实验二)===============================