Android性能优化篇之多线程并发优化

image

引言

1. Android性能优化篇之内存优化--内存泄漏

2.Android性能优化篇之内存优化--内存优化分析工具

3.Android性能优化篇之UI渲染性能优化

4.Android性能优化篇之计算性能优化

5.Android性能优化篇之电量优化(1)——电量消耗分析

6.Android性能优化篇之电量优化(2)

7.Android性能优化篇之网络优化

8.Android性能优化篇之Bitmap优化

9.Android性能优化篇之图片压缩优化

10.Android性能优化篇之多线程并发优化

11.Android性能优化篇之数据传输效率优化

12.Android性能优化篇之程序启动时间性能优化

13.Android性能优化篇之安装包性能优化

14.Android性能优化篇之服务优化

介绍

在程序开发的实践当中,为了让程序表现得更加流畅,我们肯定会需要使用到多线程来提升程序的并发执行性能。但是编写多线程并发的代码一直以来都是一个相对棘手的问题,所以想要获得更佳的程序性能,我们非常有必要掌握多线程并发编程的基础技能。

一.Thread 使用

在讲解多线程之前,我们先来讲解Thread使用几个需要注意的点:

1.Thread 中断
常用的有两种方式:
(1).通过抛出InterruptedException来中断线程
    public  static  class  MyThread extends Thread{
        private  int count=0;
        @Override
        public void run() {
            super.run();
            try{
                while(true){
                        count++;
                        System.out.println("count value:"+count);
                        if (this.interrupted() || this.isInterrupted()){
                            System.out.println("check interrupted show!");
                            throw new InterruptedException();
                        }
                }
            }catch ( InterruptedException e) {
                System.out.println("thread is stop!");
                e.printStackTrace();
            }
        }
        
    } 
(2).通过变量来中断(常用)
    public  static  class  CustomThread extends Thread{
        private  int count=0;
        private boolean isCancel = false;
        @Override
        public void run() {
            super.run();
            while(!isCancel){
                    count++;
                    System.out.println("count value:"+count);
            }
        }
        
        public synchronized void cancel(){
            isCancel = true;
        }
    } 
2.同步

我们分变量同步和代码块同步两个方面来讲解

(1).变量同步
使用volatile关键字
    /**
     * 主内存和线程内存缓存进行同步
     */
    volatile int val = 5;
    public int getVal() {
        return val;
    }
    public void setVal(int val) {
        this.val = val;
    }
使用synchronized关键字
    int val2 = 5;
    /**
     * 使用一个motinor来监听(实现资源由一个线程进行操作)
     * 主内存和线程内存缓存进行同步
     * @return
     */
    public synchronized int getVal2() {
        return val2;
    }
    public synchronized int setVal2(int val) {
        this.val2 = val;
    }
使用关键字AtomicXXXXX
    AtomicInteger mAtomicValue = new  AtomicInteger(0);
    public void setAtomicValue(int value){
        mAtomicValue.getAndSet(value);
    }
    public int getAtomicValue(){
        return mAtomicValue.get();
    }
(2).代码块同步

代码块同步分乐观锁和悲观锁来讲解

使用悲观锁时,其他线程等待,进入睡眠,频繁切换任务,消耗cpu资源
    synchronized (this) {
        .....   
    }
使用乐观锁时,失败重试,避免任务重复切换,减少cpu消耗
    ReentrantLock lock = new  ReentrantLock();
    lock.lock();
    ......
    lock.unlock();

Thread注意点就讲到这里,下面让我们进入今天的主题,多线程并发优化。

二.Android Threading

android中很多操作需要在主线程中执行,比如UI的操作,点击事件等等,但是如果主线程操作太多,占有的执行时间过长就会出现前面我们说的卡顿现象:


image1.jpg

为了减轻主线程操作过多,避免出现卡顿的现象,我们把一些操作复杂的消耗时间长的任务放到线程池中去执行。下面我们就来介绍android中几种线程的类。

1.AsyncTask

为UI线程与工作线程之间进行快速的切换提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的使用场景。
它提供了一种简便的异步处理机制,但是它又同时引入了一些令人厌恶的麻烦。一旦对AsyncTask使用不当,很可能对程序的性能带来负面影响,同时还可能导致内存泄露。(关于内存泄漏在上面已经讲过)

使用AsyncTask需要注意的问题?
(1).在AsyncTask中所有的任务都是被线性调度执行的,他们处在同一个任务队列当中,按顺序逐个执行。一旦有任务执行时间过长,队列中其他任务就会阻塞。
image3.jpg

对于上面的问题,我们可以使用AsyncTask.executeOnExecutor()让AsyncTask变成并发调度。

(2).AsyncTask对正在执行的任务不具备取消的功能,所以我们要在任务代码中添加取消的逻辑(和上面Thread类似)
(3).AsyncTask使用不当会导致内存泄漏(可以参考内存泄漏一章)
2.HandlerThread

为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。
先来了解下Looper,Handler,MessageQueue
Looper: 能够确保线程持续存活并且可以不断的从任务队列中获取任务并进行执行。
Handler: 能够帮助实现队列任务的管理,不仅仅能够把任务插入到队列的头部,尾部,还可以按照一定的时间延迟来确保任务从队列中能够来得及被取消掉。
MessageQueue: 使用Intent,Message,Runnable作为任务的载体在不同的线程之间进行传递。
把上面三个组件打包到一起进行协作,这就是HandlerThread


image2.jpg

我们先来看下源码:

    public class HandlerThread extends Thread {
        public HandlerThread(String name, int priority) {
            super(name);
            mPriority = priority;
        }

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

        public Looper getLooper() {
            if (!isAlive()) {
                return null;
            }
            // If the thread has been started, wait until the looper has been created.
            synchronized (this) {
                while (isAlive() && mLooper == null) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
            return mLooper;
        }
    }
从上面的源码发现,HandlerThread其实就是在线程中维持一个消息循环队列。下面我们看下使用:
    HandlerThread mHanderThread = new HandlerThread("hanlderThreadTest", Process.THREAD_PRIORITY_BACKGROUND);
    mHanderThread.run();
    Looper mHanderThreadLooper = mHanderThread.getLooper();

    Handler mHandler = new Handler(mHanderThreadLooper){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //子线程中执行
            ...
        }
    };
    //发送消息
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            ...
        }
    });  
3.IntentService

适合于执行由UI触发的后台Service任务,并可以把后台任务执行的情况通过一定的机制反馈给UI。
默认的Service是执行在主线程的,可是通常情况下,这很容易影响到程序的绘制性能(抢占了主线程的资源)。除了前面介绍过的AsyncTask与HandlerThread,我们还可以选择使用IntentService来实现异步操作。IntentService继承自普通Service同时又在内部创建了一个HandlerThread,在onHandlerIntent()的回调里面处理扔到IntentService的任务。所以IntentService就不仅仅具备了异步线程的特性,还同时保留了Service不受主页面生命周期影响的特点。


image5.jpg
使用IntentService需要特别注意的点:
(1).因为IntentService内置的是HandlerThread作为异步线程,所以每一个交给IntentService的任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。
(2).通常使用到IntentService的时候,我们会结合使用BroadcastReceiver把工作线程的任务执行结果返回给主UI线程。使用广播容易引起性能问题,我们可以使用LocalBroadcastManager来发送只在程序内部传递的广播,从而提升广播的性能。我们也可以使用runOnUiThread()快速回调到主UI线程。
(3).包含正在运行的IntentService的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的。
4.Loader

对于3.0后ContentProvider中的耗时操作,推荐使用Loader异步加载数据机制。相对其他加载机制,Loader有那些优点呢?

  • 提供异步加载数据机制
  • 对数据源变化进行监听,实时更新数据
  • 在Activity配置发生变化(如横竖屏切换)时不用重复加载数据
  • 适用于任何Activity和Fragment
    下面我们来看下Loader的具体使用:
    我们以获得手机中所有的图片为例:
    getLoaderManager().initLoader(LOADER_TYPE, null, mLoaderCallback);
    LoaderManager.LoaderCallbacks<Cursor> mLoaderCallback = new LoaderManager.LoaderCallbacks<Cursor>() {
        private final String[] IMAGE_COLUMNS={
                MediaStore.Images.Media.DATA,//图片路径
                MediaStore.Images.Media.DISPLAY_NAME,//显示的名字
                MediaStore.Images.Media.DATE_ADDED,//添加时间
                MediaStore.Images.Media.MIME_TYPE,//图片扩展类型
                MediaStore.Images.Media.SIZE,//图片大小
                MediaStore.Images.Media._ID,//图片id
        };

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            toggleShowLoading(true,getString(R.string.common_loading));

            CursorLoader cursorLoader = new CursorLoader(ImageSelectActivity.this,                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,IMAGE_COLUMNS,
                    IMAGE_COLUMNS[4] + " > 0 AND "+IMAGE_COLUMNS[3] + " =? OR " +IMAGE_COLUMNS[3] + " =? ",
                    new String[]{"image/jpeg","image/png"},IMAGE_COLUMNS[2] + " DESC");
            return cursorLoader;
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            if(data != null && data.getCount() > 0){
                ArrayList<String> imageList = new ArrayList<>();

                if(mShowCamera){
                    imageList.add("");
                }
                while (data.moveToNext()){
                    String path = data.getString(data.getColumnIndexOrThrow(IMAGE_COLUMNS[0]));
                    imageList.add(path);
                    Log.e("ImageSelect", "IIIIIIIIIIIIIIIIIIII=====>"+path);
                }
                //显示数据
                showListData(imageList);
                toggleShowLoading(false,getString(R.string.common_loading));
            }
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {  
        }   

onCreateLoader() 实例化并返回一个新创建给定ID的Loader对象
onLoadFinished() 当创建好的Loader完成了数据的load之后回调此方法
onLoaderReset() 当创建好的Loader被reset时调用此方法,这样保证它的数据无效
LoaderManager会对查询的操作进行缓存,只要对应Cursor上的数据源没有发生变化,在配置信息发生改变的时候(例如屏幕的旋转),Loader可以直接把缓存的数据回调到onLoadFinished(),从而避免重新查询数据。另外系统会在Loader不再需要使用到的时候(例如使用Back按钮退出当前页面)回调onLoaderReset()方法,我们可以在这里做数据的清除等等操作。

5.ThreadPool

把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。
线程池适合用在把任务进行分解,并发进行执行的场景。
系统提供ThreadPoolExecutor帮助类来帮助我们简化实现线程池。


image4.jpg

使用线程池需要特别注意同时并发线程数量的控制,理论上来说,我们可以设置任意你想要的并发数量,但是这样做非常的不好。因为CPU只能同时执行固定数量的线程数,一旦同时并发的线程数量超过CPU能够同时执行的阈值,CPU就需要花费精力来判断到底哪些线程的优先级比较高,需要在不同的线程之间进行调度切换。
一旦同时并发的线程数量达到一定的量级,这个时候CPU在不同线程之间进行调度的时间就可能过长,反而导致性能严重下降。另外需要关注的一点是,每开一个新的线程,都会耗费至少64K+的内存。为了能够方便的对线程数量进行控制,ThreadPoolExecutor为我们提供了初始化的并发线程数量,以及最大的并发数量进行设置。

    /**
     * 核心线程数
     * 最大线程数
     * 保活时间
     * 时间单位
     * 任务队列
     * 线程工厂
     */
    threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            linkedBlockingQueue, sThreadFactory);
    threadPoolExecutor.execute(runnable);
我们知道系统还提供了Executors类中几种线程池,下面我们来看下这些线程池的缺点:

newFixedThreadPool 和 newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
newCachedThreadPool 和 newScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM

我们看到这些线程池但是有缺点的,所以具体使用那种方式实现要根据我们的需求来选择。

如果想要避开上面的问题,可以参考OKHttp中线程池的实现,OKHttp中队线程调度又封装了一层,使用安全且方便,有兴趣的可以去看看源码。

三.线程优先级

Android系统会根据当前运行的可见的程序和不可见的后台程序对线程进行归类,划分为forground的那部分线程会大致占用掉CPU的90%左右的时间片,background的那部分线程就总共只能分享到5%-10%左右的时间片。之所以设计成这样是因为forground的程序本身的优先级就更高,理应得到更多的执行时间。


image6.jpg

默认情况下,新创建的线程的优先级默认和创建它的母线程保持一致。如果主UI线程创建出了几十个工作线程,这些工作线程的优先级就默认和主线程保持一致了,为了不让新创建的工作线程和主线程抢占CPU资源,需要把这些线程的优先级进行降低处理,这样才能给帮组CPU识别主次,提高主线程所能得到的系统资源。

在Android系统里面,我们可以通过android.os.Process.setThreadPriority(int)设置线程的优先级,参数范围从-20到24,数值越小优先级越高。Android系统还为我们提供了以下的一些预设值,我们可以通过给不同的工作线程设置不同数值的优先级来达到更细粒度的控制。


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

推荐阅读更多精彩内容