Android的线程和线程池

线程

线程在Android中是一个很重要的概念,从用途上来说,线程分为主线程子线程,主线程主要处理和界面相关的事情,而子线程则往往用于执行耗时操作。由于Android的特性,如果在主线程中执行耗时操作那么会导致程序无法及时响应(ANR),因此耗时操作必须放在子线程中执行。

除了Thread本身除外,在Android中可以扮演线程角色的的还有很多,比如AsyncTaskIntentService,同时HandlerThread也是一种特殊的线程。尽管他们的表现形式有别于传统的线程,但是他们的本质仍然是传统的线程。不同形式的线程虽然都是线程,但是它们仍然有不同的特性和使用场景。下面将分别来剖析上面所提到的三种线程:

1. AsyncTask

AsyncTask是一种轻量级的异步任务类。它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新UI。从实现来说,AsyncTask封装了ThreadHandler通过AsyncTask可以更方便地执行后台任务以及在主线程中访问UI,但是AsyncTask并不适合进行特别耗时的后台任务,对于特别耗时的来说,建议使用线程池。

AsyncTask是一个抽象的泛型类,他提供了ParamsProgressResult三个泛型参数,分别用于表示参数的类型后台任务的执行进度的类型返回结果的类型。如果AsyncTask确实不需要传递具体的参数,那么这三个泛型参数可以用Void来代替。AsyncTask这个类的声明如下所示:

public abstract class AsyncTask<Params, Progress, Result>

AsyncTask提供了4个核心方法,它们的含义如下所示:

  • onPreExecute(),在主线程中执行,在异步任务执行之前,此方法会被调用,一般可用于做一些准备工作。
  • doInBackground(Params... params),在线程池中执行,此方法用于执行异步任务,params参数表示异步任务的输入参数。在此方法中可以通过publishProgress(Progress... values)来更新任务进度publishProgress会调用onProgressUpdate方法。另外此方法需要返回计算结果给onPostExecute()方法。
  • onProgressUpdate(Progress... values),在主线程中执行,当后台任务的执行进度发生改变时此方法会被调用。
  • onPostExecute(Result result),在主线程中执行,在异步任务执行之后,此方法会被调用,其中result参数是后台任务的返回值,即doInBackground的返回值。

上面几个方法,onPreExecute()先执行,接着是doInBackground,最后是onPostExecute。除了上述四个方法外,AsyncTask还提供了onCanceled()方法,他同样在主线程中进行,当异步任务被取消时,onCanceled()方法会被调用,这个时候onPostExecute则不会被调用。下面提供一个典型的示例:

public class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    @Override
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            if (isCancelled()) {
                break;
            }
        }
        return totalSize;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        setProgressPercent(values[0]);
    }

    @Override
    protected void onPostExecute(Long result) {
        showDialog("Downloaded" + result + " bytes");
    }


}

当要执行该任务是,可通过如下方式:

 new DownloadFilesTask().execute(url1,url2,url3);

AsyncTask在具体使用的过程中有一些条件限制,主要有如下几点:

  • AsyncTask类必须在主线程中加载。
  • AsyncTask对象必须在主线程中创建。
  • execute方法必须在UI线程调用。
  • 不要在程序中直接调用onPreExecute()doInBackgroundonProgressUpdateonPostExecute方法。
  • 一个AsyncTask对象只能执行一次,即只能调用一个execute方法,否则会报运行时异常(IllegalStateException)。
  • AsyncTask默认是串行执行任务的,但是可以通过executeOnExecutor方法来让他并行的执行任务。

关于AsyncTask的介绍就到这了,如果想要深入了解其工作原理,请阅读:AsyncTask的工作原理(源码剖析)

2. HandlerThread

HandlerThread继承了Thread,它是一种可以使用HandlerThread,它的实现也很简单,就是run方法中通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环,这样在实际的使用中就允许在HandlerThread中创建Handler了。HandlerThreadrun方法如下所示:

@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

HandlerThread的实现来看,它和普通的Thread有显著的不同之处。普通的Thread主要用于在run方法中执行一个耗时任务,而HandlerThread在内部创建了消息队列,外界需要通过Handler的消息方式来通知HandlerThread执行一个具体的任务。HandlerThread是一个很有用的类,它在Android中的一个具体使用场景是IntentService由于HandlerThreadrun方法是一个无限循环,因此当明确不需要再使用HandlerThread时,可以通过它的quit或者quitSafely方法来终止线程的执行。

3. IntentService

IntentService是一个特殊的Service,它继承了Service并且它是一个抽象类,因此必须创建它的子类才能使用IntentServiceIntentService用于执行后台耗时的任务,当任务执行后它会自动停止,同时由于IntentService是服务的原因,这导致它的优先级比单纯的线程要高很多,所以IntentService比较适合执行一些高优先级的后台任务,因为它优先级高不容易被系统杀死。在实现上,IntentService封装了HandlerThreadHandler,这一点可以从它的onCreate方法看出来:

@Override
public void onCreate() {
    // TODO: It would be nice to have an option to hold a partial wakelock
    // during processing, and to have a static startService(Context, Intent)
    // method that would launch the service & hand off a wakelock.

    super.onCreate();
    HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
    thread.start();

    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);
}

IntentService被第一次启动时,它的onCreate方法会被调用,onCreate方法会创建一个HandlerThread,然后使用它的Looper来构造一个Handler对象mServiceHandler,这样通过mServiceHandler发送的消息最终都会在HandlerThread中执行,从这个角度来看,IntentService也可以用于执行后台任务。每次启动IntentService,它的onStartCommand方法就会调用一次,IntentServiceonStartCommand中处理每个后台任务的Intent。下面看一下onStartCommand方法是如何处理外界的Intent的,onStartCommand调用了onStartonStart方法的实现如下:

@Override
public void onStart(@Nullable Intent intent, int startId) {
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent;
    mServiceHandler.sendMessage(msg);
}

可以看出,IntentService仅仅是通过mServiceHandler发送了一个消息,这个消息会在HandlerThread中被处理。mServiceHandler收到消息后,会将Intent对象传递给onHandlerIntent方法去处理。注意这个Intent对象的内容和外界的startService(intent)中的intent内容是完全一致的,通过这个Intent对象即可解析出外界启动IntentService时所传递的参数,通过这些参数就可以区分具体的后台任务,这样在onHandlerIntent方法中就可以对不同的后台任务做处理了。当onHandlerIntent方法执行结束后,IntentService会通过stopSelf(int startId)方法来尝试停止服务。这里之所以采用stopSelf(int startId)而不是stopSelf()来停止服务,是因为stopSelf()会立刻停止服务,而这个时候可能还有其他消息未处理,stopSelf(int startId)则会等待所有的消息都处理完毕后才终止服务。
一般来说,stopSelf(int startId)在尝试停止服务之前会判断最近启动服务的次数是否和startId相等,如果相等就立刻停止服务,不相等则不停止服务。ServiceHandler的实现如下所示:

private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        onHandleIntent((Intent)msg.obj);
        stopSelf(msg.arg1);
    }
}

IntentServiceonHandlerIntent方法是一个抽象方法,它需要我们在子类中实现,它的作用是从Intent参数中区分具体的任务并执行这些任务。如果目前只存在一个后台任务,那么onHandlerIntent方法执行完这个任务后,stopSelf(int startId)就会直接停止服务;如果目前存在多个后台任务,那么当onHandlerIntent方法执行完最后一个任务时,stopSelf(int startId)就才直接停止服务。另外,由于每执行一个后台任务就必须启动一次IntentService,而IntentService内部则通过消息的方式向HandlerThread请求执行任务,Handler中的Looper是按顺序处理消息的,这就意味着IntentService也是顺序执行后台任务的,当多个后台任务同时存在时,这些后台任务会按照外界发起的顺序排队执行。
下面通过一个例子来验证这一点,首先实现一个IntentService类:

public class LocalIntentService extends IntentService {

    private static final String TAG = "LocalIntentService";

    /**
     * Creates an IntentService.  Invoked by your subclass's constructor.
     */
    public LocalIntentService() {
        super(TAG);
    }


    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        String action = intent.getStringExtra("task_action");
        Log.d(TAG, "receive task: " + action);
        SystemClock.sleep(3000);
        if ("com.yu.hu.action.TASK1".equals(action)) {
            Log.d(TAG, "handle task: " + action);
        }
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "service onDestroy");
        super.onDestroy();
    }
}

然后添加一个按钮并写上点击事件:

Button button = findViewById(R.id.btn_start_task);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    
        Intent intent = new Intent(MainActivity.this, LocalIntentService.class);
        intent.putExtra("task_action", "com.yu.hu.action.TASK1");
        startService(intent);
    
        intent.putExtra("task_action", "com.yu.hu.action.TASK2");
        startService(intent);
    
        intent.putExtra("task_action", "com.yu.hu.action.TASK3");
        startService(intent);
    
    }
});

运行结果:


由此可以验证出确实是排队执行的,且当TASK3执行完成后,LocalIntentService才执行onDestroy,服务才会停止。

线程池

在操作系统中,线程是操作系统调度的最小单元,同时线程又是一种受限的系统资源,即线程不可能无限制的产生,并且线程的创建和销毁都会有相应的开销。所以此时线程池就派上了用场。一个线程池中会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。
线程池主要有以下三个优点

  • 重用线程池中的线程,避免因为线程的创建和销毁所带来的的性能开销。
  • 有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
  • 能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。

Android中的线程池来源于Java,主要是通过Executor来派生特定类型的线程池,不同种类的线程池又各有各的特性。Executor是一个接口,真正的线程池的实现为ThreadPoolExecutorThreadPoolExecutor提供了一系列的参数来配置线程池,通过不同的参数可以创建不同的线程池。推荐阅读:ThreadPoolExecutor配置细节

Android中有四类常见的具有不同功能特性的线程池,它们都直接或间接地通过配置ThreadPoolExecutor来实现自己的功能特性。它们分别是:FixedThreadPoolCachedThreadPoolScheduledThreadPool以及SingleThreadExecutor。下面一一简单介绍下这四种线程池:

  • FixedThreadPool
    通过ExecutorsnewFixedThreadPool方法来创建。它是一种线程数量固定的线程池,当线程处于空闲状态时,它们并不会被回收,除非线程池被关闭了。当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有线程空闲下来。由于FixedThreadPool只有核心线程并且这些核心线程不会被回收,这意味着它能够更加快速地响应外界的请求。newFixedThreadPool方法的实现如下,可以发现FixedThreadPool只有核心线程并且这些核心线程没有超时机制,另外任务队列也是没有大小限制的。
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}
  • CachedThreadPool
    通过ExecutorsnewCachedThreadPool方法来创建。它是一种线程数量不定的线程池,它只有非核心线程,并且其最大线程数为 Integer.MAX_VALUE。由于 Integer.MAX_VALUE是一个很大的数,实际上就相当于最大线程数可以任意大。当线程池的线程都处于活动状态时,线程池会创建新的线程来处理新任务,否则就会利用空闲的线程来处理新任务。线程池中的空闲线程都有60秒的超时机制,超过60秒闲置线程就会被回收。FixedThreadPool不同的是,CachedThreadPool的任务队列相当于一个空集合,这将导致任何任务都会立即被执行,因为在这种场景下SynchronousQueue是无法插入任务的。SynchronousQueue是一个非常特殊的队列,很多情况下可以把它理解为一个无法存储元素的队列。
    CachedThreadPool的特性来看,这类线程池比较适合执行大量的耗时较少的任务。当整个线程池都处于闲置状态时,线程池中的线程都会超时而被停止,这个时候CachedThreadPool之中实际上是没有任何线程的,它几乎是不占用任何系统资源的CachedThreadPool方法的实现如下:
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}
  • ScheduledThreadPool
    通过ExecutorsnewScheduledThreadPool方法来创建。它的核心线程数是固定的,而非核心线程数是没有限制的,并且当非核心线程闲置时会被立即回收。ScheduledThreadPool这类线程池主要用于执行定时任务和具有固定周期的重复任务newScheduledThreadPool方法的实现如下所示:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
            DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
            new DelayedWorkQueue());
}
  • SingleThreadExecutor
    通过ExecutorsnewSingleThreadExecutor方法来创建。这类线程池内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行SingleThreadExecutor的意义在于统一所有的外界任务到一个线程中,这使得在这些任务之间不需要处理线程同步的问题。SingleThreadExecutor方法的实现如下所示:
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}

上面对Android中常见的4种线程池进行了详细的介绍,除了上面系统提供的4种线程池外,也可以根据实际需要灵活地配置线程池。最后附上一段代码用于演示这四种线程池的典型使用方法:

Runnable command = new Runnable() {
    @Override
    public void run() {
        SystemClock.sleep(2000);
    }
};

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
fixedThreadPool.execute(command);

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
cachedThreadPool.execute(command);

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
//2000ms后执行command
scheduledThreadPool.schedule(command, 2000, TimeUnit.MILLISECONDS);
//延迟10ms后,每隔1000ms执行一次command
scheduledThreadPool.scheduleAtFixedRate(command, 10, 1000, TimeUnit.MILLISECONDS);

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
singleThreadExecutor.execute(command);

参考书籍: 《Android开发艺术探索》第11章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350