博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android中的线程池和AsyncTask异步任务(二)
阅读量:4094 次
发布时间:2019-05-25

本文共 5862 字,大约阅读时间需要 19 分钟。

前言:上一篇博文说到了线程池和AsyncTask的联系和基本的使用。也提到了AsyncTask的底层实现实际上是封装了2个线程池(一个用于排队,一个用于执行任务)和一个Handler,这两个线程池具体是怎么工作的呢?还有提到了AsyncTask并不适合执行特别耗时的任务,若是执行这种任务又会怎么样呢?接下来走进源码,继续剖析AsyncTask的工作原理。                                              

一、认识AsyncTask中的两个线程池

a.SERIAL_EXECUTOR/sDefaultExecutor(SerialExecutor实例),这是AsyncTask中用于对任务进行排队的线程池。
源码中SerialExecutor是这样定义的(添加了必要的注释):

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有着本质差别。前者是线程池中的任务队列,而后者是待进入线程池的队列。

二、AsyncTask的串行任务执行和并行任务执行

a.先来看一下默认的执行方式(串行),可以在UI线程中调用execute()方法,这个方法有2个不同的重载版本,如下:
//传入可变参数,但是注意仍然是一个Runnable任务

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,看看是否会拒绝任务?

好的,不负众望,拒绝了我的任务。。。并行执行得证!

=========================分割线(实验二)===============================

三、总结

a.通过execute()方法执行AsyncTask执行异步任务的时候,默认是串行方式执行,实际上执行任务的线程池中始终只有一个任务处于执行中,而在此线程池外边排队的线程数目没有限制(取决于内存)。
b.可以调用executeOnExecutor()方法,传入THREAD_POOL_EXECUTOR参数使得任务并发执行,但是这时候必须处理线程的同步问题,并且线程池中的线程数量应该小于等于128+10,否则会抛出异常。不过响应地,速度有所提升。
c.为什么AsyncTask不适合特别耗时的任务呢?这要从AsyncTask的设计初衷来讨论,这个类的设计是为了更好地在执行耗时任务的同时更新UI线程的(这从AsyncTask的方法可以看出来)。当任务特别耗时的时候,这种更新就会显得失去了它原本的意义。因此特别耗时的任务一般采用线程池。
d.因此对于AsyncTask使用上的考虑,一般是在需要和UI线程交互的耗时任务上面,否则应该考虑线程池。因为AsyncTask不是要求串行执行,就是在并行执行上对线程数量作了约束。

你可能感兴趣的文章
mapReduce(3)---入门示例WordCount
查看>>
hbase(3)---shell操作
查看>>
hbase(1)---概述
查看>>
hbase(5)---API示例
查看>>
SSM-CRUD(1)---环境搭建
查看>>
SSM-CRUD(2)---查询
查看>>
SSM-CRUD (3)---查询功能改造
查看>>
Nginx(2)---安装与启动
查看>>
springBoot(5)---整合servlet、Filter、Listener
查看>>
C++ 模板类型参数
查看>>
C++ 非类型模版参数
查看>>
设计模式 依赖倒转原则 & 里氏代换原则
查看>>
DirectX11 光照
查看>>
图形学 图形渲染管线
查看>>
DirectX11 计时和动画
查看>>
DirectX11 光照与材质的相互作用
查看>>
DirectX11 法线向量
查看>>
DirectX11 兰伯特余弦定理(Lambert)
查看>>
DirectX11 漫反射光
查看>>
DirectX11 环境光
查看>>