Android学习笔记:多个AsyncTask实例的并发问题
AsyncTask是Android给开发者提供的一个简单轻量级的多线程类,通过它我们可以很容易新建一个线程让在后台做一些耗时的操作(如IO操作、网络访问等),并在这个过程中更新UI。之所以说它轻量级,是因为不需要直接使用Handler、Thread等知识,使用起来比较简单,但也失去了一些灵活性,对于一些复杂的场景处理起来不方便。
如果一个APP进程中同时只创建和运行一个AsyncTask实例,则不会有任何问题。但如果在一个进程中如果有多个AsyncTask任务同时在执行,问题就比较复杂了。下面我们通过例子来看(我们例子是在Android 4中运行的)。
一、测试1(默认多个Task是串行执行的)
1、创建一个默认的app工程
2、创建一个类继承AsyncTask,代码如下
package com.example.asynctaskdemo; import android.os.AsyncTask; public class MyAsyncTask extends AsyncTask<String, Void, String>{ private String name; public MyAsyncTask(String name){ this.name = name; } @Override protected String doInBackground(String... params) { System.out.println(name+" is run "+System.currentTimeMillis()+" thread id "+Thread.currentThread().getId()); try { Thread.sleep(1000*10); } catch (InterruptedException e) { e.printStackTrace(); } return null; } }
3、在Activity的onCreate方法中使用该Task,代码如下
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); for(int i=1;i<5;i++){ MyAsyncTask task = new MyAsyncTask("task"+i); task.execute(new String[0]); } }
我们在调试窗口,观察MyAsyncTask打印信息的间隔和顺序。发现这创建的4个任务是串行执行的,并不是并发的。
研究了AsyncTask的实现细节,在创建一个AsyncTask并通过其execute方法启动执行时,AsyncTask并不是创建一个独立的线程去执行。AsyncTask是通过线程池来管理和调度进程中的所有Task的。
在 Android2.3以前的版本(SDK/API 大于等于10的版本)
多个AsyncTask任务是并发执行的,也就是说如果启动多个task,则会并发执行。但并发执行的数量取决于AsyncTask内部的线程池限制数量。如果超过了这个限额,新的任务只能等待。
在Android 3.0及以后版本(SDK/API 大于等于11的版本)
Google从Android 3.0开始对AsyncTask的调度执行做出了一些变化,对于execute提交的任务,按先后顺序每次只运行一个。也就是说它是按提交的次序,每次只启动一个线程执行一个任务,完成之后再执行第二个任务,也就是相当于只有一个后台线程在执行所提交的任务。上面的例子就验证了这一点。
二、让AsyncTask并发执行
因为默认情况下多个task是串行的,那怎么样让并发执行呢?AsyncTask增加了一个新的接口executeOnExecutor,这个接口允许开发者提供自定义的线程池来运行和调度Thread。我们把上面代码改下:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); for(int i=1;i<11;i++){ MyAsyncTask task = new MyAsyncTask("task"+i); //task.execute(new String[0]); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new String[0]); } }
这里我们使用了executeOnExecutor方法代替了execute方法。并且executeOnExecutor方法的第一个参数是一个预定义的线程池。这时这几个task就可以并发执行了。这时我们观察打印的结果,发现有5个任务并发执行,可以看出有5个不同的线程号,查看AsyncTask的源码,发现并发线程数跟设备的cpu数量是有关的,因此不同的设备上可能看到的结果不完全一致,这点需要注意。只有前面的5个任务执行完后,才会执行后面的,并且通过打印的线程号可以看出,后面执行的任务是重用原来的线程,并没有创建新的线程,这就是线程池的作用。
我们再来改下代码,使用Android提供的另外一个预定义的线程池。代码如下:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); for(int i=1;i<11;i++){ MyAsyncTask task = new MyAsyncTask("task"+i); //task.execute(new String[0]); task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, new String[0]); } }
观察打印信息我们可以发现,这和调用execute方法一样,每个任务都是串行执行的。并且这个过程中最多创建了5个新的线程。
三、自定义线程池
我们可以利用java.util.concurrent.Executors中的各种静态方法创建供AsyncTask执行的线程池 ,可以指定线程的数量和调度的方式。其方法很多,我们这里介绍其中两种较为常用的。
1、让每个AsyncTask任务都单独起一个线程执行,也就是说所有的都是并发的。代码如:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); for(int i=1;i<11;i++){ MyAsyncTask task = new MyAsyncTask("task"+i); task.executeOnExecutor(newCachedThreadPool, new String[0]); } }
通过观察打印可以看出,这多个任务都是并发执行的。
2、创建指定线程数量的线程池,并发数上限就是指定的线程数。但新任务产生,没有空闲的线程,就只能等待。代码如:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3); for(int i=1;i<11;i++){ MyAsyncTask task = new MyAsyncTask("task"+i); task.executeOnExecutor(newFixedThreadPool, new String[0]); } }
通过观察可以看出,有3个线程在并发执行。
总结下,从上面的例子中可以看出,如果一个进程中存在多个TASK需要并发执行的情况,那就需要用到AsyncTask一些更深的知识,需要考虑的问题更多。