首页 > 代码库 > Android中使用多线程的各种姿势

Android中使用多线程的各种姿势

写在前面:内容主要为黄岳钊老师视频分享课的学习笔记。


1)为什么需要多线程处理?

  • 解决耗时任务
    文件IO、联网请求、数据库操作、RPC

  • 提高并发能力
    同一时间处理更多事情

  • 防止ANR
    InputDispatching Timeout:输入事件分发超时5s(触摸或按键)
    Service Timeout:服务20s内未执行完
    BroadcastQueue Timeout:前台广播10s内未执行完
    ContentProvider Timeout:内容提供者执行超时

  • 避免掉帧
    要达到每秒60帧,每帧必须16ms处理完


2)使用多线程的几种姿势

  • Thread
    ① new Thread,重载run方法;
    ② 实现Runable接口,作为参数传给Thread。
public static void main(String[] args){
        //Android中   UI线程的Id恒定为1
        System.out.println("UI Thread Id : "+Thread.currentThread().getId());

        new Thread(){
            @Override
            public void run() {
                // TODO: 2016/12/2

                System.out.println("run in thread "+Thread.currentThread().getId());
            }
        }.start();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // TODO: 2016/12/2
                System.out.println("run in thread "+Thread.currentThread().getId());
            }
        };

        new Thread(runnable).start();
    }

技术分享

  • AsyncTask

android特有的轻量级异步任务类,它可以在线程池中执行后台任务,然后把执行进度和结果传递给主线程中更新UI。但不适合特别耗时的后台任务。

(android特有,为便于更新UI,由google推出)

public class MyTask extends AsyncTask<String,Integer,String> {

    String TAG = "haha";

    @Override
    protected void onPreExecute() {
        // TODO: 2016/12/2
        Log.d(TAG,"onPreExecute run in thread "+Thread.currentThread().getId());
    }

    //只有这个方法在后台执行,其他方法都在UI线程
    @Override
    protected String doInBackground(String... params) {
        // TODO: 2016/12/2
        Log.d(TAG,"doInBackground run in thread "+Thread.currentThread().getId());
        //传入的参数和execute中传入参数相同
        Log.d(TAG,"input param "+params[0]);

        //此方法将回调onProgressUpdate 用于进度更新
        publishProgress(5);
        //此return的信息将传递到onPostExecute
        return "task done!";
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // TODO: 2016/12/2
        Log.d(TAG,"onProgressUpdate run in thread "+Thread.currentThread().getId());

        Log.d(TAG,"update progress "+values[0]);

    }

    @Override
    protected void onPostExecute(String s) {
        // TODO: 2016/12/2
        Log.d(TAG,"onPostExecute run in thread "+Thread.currentThread().getId());
        Log.d(TAG,"onPostExecute input "+s);

    }

}

技术分享

分析:AsyncTask只有doInBackground 在子线程中执行,其他都是在UI线程。

使用注意点:

1)想象一种情况:在一个Activity页面,如果发起了AsyncTask任务,然后页面离开/销毁了,此时如果doInBackground没执行完,会有什么问题?
首先,AsyncTask白白消耗资源,结果已经用不上了,因为UI也不在;

2)那么如何优雅的终止AsyncTask呢?
鉴于以上的问题,一般我们要在Activity onDestory的时候cancel掉AsyncTask任务。

3)关于cancel()方法
cancel()方法并非是直接停止Asynctask后台线程,而是发送一个停止线程的状态位,
因而需要在doInBackground 不断的检查此状态位。
如:if(isCancelled()) return null; // Task被取消了,马上退出

  • HandlerThread

HandlerThread实际上是一个带有Looper的Thread,从而可向子线程传递消息。

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //HandlerThread 其实就是一个带有Looper的Thread
        HandlerThread handlerThread = new HandlerThread("handler-thread");
        handlerThread.start();

        Log.d(TAG,"handlerthread id :"+handlerThread.getId());

        //用handlerThread的looper生成handler  用于向子线程中发送消息、runnable 等
        Handler handler = new Handler(handlerThread.getLooper()){
            @Override
            public void handleMessage(Message msg) {
                Log.d(TAG,"handleMessage received in thread "+Thread.currentThread().getId());

                Log.d(TAG,"received message is "+msg.obj);
            }
        };//必须先start thread

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"this runnable run in thread "+Thread.currentThread().getId());
            }
        };
        //使runnable在子线程(即handlerThread线程执行)
        handler.post(runnable);
        //此消息发送至handlerThread线程
        Message message = new Message();
        message.obj = "test message";
        handler.sendMessage(message);
    }

技术分享

  • ExecutorService

首先来看下java.util.concurrent包下关于并发编程的几个重要类和接口:Executor,Executors、ExecutorService。

Executor(执行器):
如下,是一个接口,提供了execute()方法,用于执行任务,而不用显式的创建线程(使用其内部的线程池来完成操作)。
其最常用的子接口ExecutorService,扩展了shutdown()、submit()等方法,用于管理终止任务。

技术分享
ExecutorService:
shutdown:完成已提交的任务后(调用后不能再提交新任务,且可以等待已提交任务执行完),关闭ExecutorService以回收资源。
submit:返回Future对象,用于取消执行或等待完成。
invokeAll:用于批量执行。

技术分享
Executors:
主要用于提供线程池相关的操作,返回Executor执行器。
技术分享

再来看几种不同的线程池:FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadPool。

FixedThreadPool:
通过Executors.newFixedThreadPool()创建,是一种线程数量固定的线程池。线程并不会被回收,除非线程池被关闭。当所有线程处于活动状态,新任务会被阻塞,直至有线程可用。
适于线程数固定。

CachedThreadPool:
通过Executors.newCachedThreadPool()创建,线程数量不定。只有非核心线程,最大线程数为Integer.MAX_VALUE。
空闲线程有超时机制,时长为60s,超过60s闲置线程会被回收。
当没有空闲线程时,对新任务会创建新线程,从而保证所有任务立即被执行。
适于执行大量且耗时较短的任务,线程数按需创建,用完回收。

 Runnable runnable2 = new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"runnable2 run in thread "+Thread.currentThread().getId());

                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Log.d(TAG,"runnable2 is interrupted");
                }
                Log.d(TAG,"任务完成!");

            }
        };

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(runnable2);
        executorService.execute(runnable2);
        try {
            //休眠10s   前面的任务都执行完  且线程都没有被回收,新添加的任务将使用现有的空闲线程
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //将复用前面的两个空闲线程
        executorService.execute(runnable2);
        executorService.execute(runnable2);
        try {
            //休眠70s  空闲线程被回收,新添加的任务将启动新线程
            TimeUnit.SECONDS.sleep(70);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //所有的空闲线程已空闲60s而被回收  因而新添加的任务将创建新线程
        executorService.execute(runnable2);
        executorService.shutdown();

技术分享

ScheduledThreadPool:
通过Executors.newScheduledThreadPool()创建,核心数量固定且非核心线程数量无限制,当非核心线程闲置时立即被回收。
适于执行定时任务和固定周期的重复任务。

Runnable runnable2 = new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"runnable2 run in thread "+Thread.currentThread().getId());

                try {
                    //睡眠1min  为了便于观察结果
                    TimeUnit.SECONDS.sleep(60);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Log.d(TAG,"runnable2 is interrupted");
                }
                Log.d(TAG,"任务完成!");

            }
        };

        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        scheduledExecutorService.schedule(runnable2,1,TimeUnit.SECONDS);
        scheduledExecutorService.schedule(runnable2,1,TimeUnit.SECONDS);
        //创建的时候核心线程数为2  因此任务数目大于2时会等待前面的任务执行完才会开始执行新任务
        scheduledExecutorService.schedule(runnable2,1,TimeUnit.SECONDS);

        scheduledExecutorService.shutdown();

技术分享
(注意观察任务完成的时间和线程id)

SingleThreadPool:
通过 Executors.newSingleThreadExecutor()创建,只有一个核心线程,因而所有任务顺序执行,因而不用处理线程同步问题。

 Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"runnable1 run in thread "+Thread.currentThread().getId());
            }
        };

        Runnable runnable2 = new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"runnable2 run in thread "+Thread.currentThread().getId());

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Log.d(TAG,"runnable2 is interrupted");
                }
            }
        };


        ExecutorService singleThreadTool = Executors.newSingleThreadExecutor();

        singleThreadTool.execute(runnable1);
        singleThreadTool.execute(runnable1);

        Future future = singleThreadTool.submit(runnable2);

        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //停止runnable2
        future.cancel(true);
        //关闭线程池
        singleThreadTool.shutdown();
        //此任务不会被执行
        singleThreadTool.submit(runnable1);

技术分享

  • IntentService

IntentService是一种特殊的Service,继承自Service且是一个抽象类。其子类必须实现onHandleIntent方法。
IntentService底层通过HandlerThread实现。onHandleIntent方法在后台运行。
由于IntentService是服务,不容易被系统杀死,适于执行高优先级的后台任务。


3)使用多线程的公共问题

  • 生命周期
    运行时间长,不受UI生命周期限制,引用外部UI类、Activity容易导致内存泄漏。
  • UI操作
  • 通常不能直接操作UI,需要配合Handler。
  • 线程同步问题

4)适用场景

  • Thread
    默认优先级与创建时所在线程相同,适合单次耗时任务,执行完自己会结束线程。
  • HandlerThread
    默认优先级与UI线程同,使用过多会导致UI卡顿;
    不使用时需手动调用HandlerThread.quit()退出。

  • AsyncTask
    本质是对ThreadPoolExecutor和FutureTask的封装,默认为后台线程优先级。
    提供了取消、进度通知、修改UI的能力,但不易使用。
    创建数量超过128个,会抛出异常。
    兼容性:1.6到2.x并发执行,3.0起串行执行。4.1之前首次使用必须要在UI线程创建。

  • ExecutorService
    优先级为默认优先级,不使用时需调用shutdown回收线程池。

  • IntentService
    不依赖UI,适于后台运行,执行完任务自动退出。


5)推荐的多线程使用方式

  • 普通场景,要求高并发
    Executors.newFixedThreadPool()高复用
    Executors.newCachedThreadPool()低延迟

  • 需要顺序执行
    使用HandlerThread

  • 需要后台执行
    IntentService
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Android中使用多线程的各种姿势