深入了解Android多线程(二)线程的性能优化

前言

在上一篇文章中我们知道了在多线程并发时,可以使用Synchronized加锁,以保证资源的互斥访问。但是使用锁会引起线程上下文的切换开销,同时需要注意的是,线程的创建和销毁是有一定的性能损耗的,如果程序中多处使用了多线程,该如何优化呢?这就是本文所要探讨的主要内容。

【深入了解Android多线程】当前分为三个部分,这三个部分一起阅读,能更好的帮助你理解,Android在多线程方面设计与优化。

锁性能的优化

看这样一个例子

    private int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized void setValue(int value) {
        this.value = value;
    }

阅读过上一篇文章,我们很容易理解,如果线程A正在访问setValue(),即使线程A没有在访问getValue(),其他线程也无法访问getValue()。上一篇文章中提出给两个方法指定不同的监视器,其实Java 还提供了一种弱形式的同步,也就是使用 volatile

volatile

1.该关键字确保了对一个变量的更新对其他线程马上可见。当一个变量被声明为 volatile 的时候,线程写入变量的时候不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存,当其他线程读取该共享变量的时候,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
注意:volatile并不是锁!在保证内存可见性上 synchronized 和使用 volatile 是等价的,但是volatile并没有保证操作的原子性。
使用场景:当一个变量的值的改变,不依赖它原来的值时,可以使用volatile替代synchronized。
上面的例子中value的改变和它本身的值无关,所以可以直接使用volatile

    private volatile int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

我们将上面的例子再做一些修改

    private volatile int value;

    public int getValue() {
        //累加
        return value++;
    }

    public void setValue(int value) {
        this.value = value;
    }

这里value出现了一个累加操作,value的改变需要依赖其自身的值,用volatile就无法保证它的原子性,在Android Studio编辑器也会提示,这段代码不具有原子性


屏幕快照.png

为了保证getValue()的原子性,我们就需要重新使用synchronized

    private int value;

    public synchronized int getValue() {
        return value++;
    }

    public synchronized void setValue(int value) {
        this.value = value;
    }

但是synchronized使getValue(读操作)和setValue(写操作)共用一个监视器,降低了并发度。java的设计者考虑到这种情况,给出了一种并发度更高的锁—读写分离锁

读写分离锁

读写分离锁顾名思义就是将读取和写入加锁的操作进行分离,从而大大提高系统性能的。
使用读写锁改造一下上面的例子。

    private ReentrantReadWriteLock mReentrantReadWriteLock = new ReentrantReadWriteLock();
    //读锁
    private ReentrantReadWriteLock.ReadLock mReadLock = mReentrantReadWriteLock.readLock();
    //写锁
    private ReentrantReadWriteLock.WriteLock mWriteLock = mReentrantReadWriteLock.writeLock();

    private int value;

    public int getValue() {
        mReadLock.lock();
        try {
            return value++;
        } finally {
            //解除锁的操作必须在finally代码块中
            mReadLock.unlock();
        }
    }

    public void setValue(int value) {
        mWriteLock.lock();
        try {
            this.value = value;
        } finally {
            //解除锁的操作必须在finally代码块中
            mWriteLock.unlock();
        }
    }

读写锁的基本使用就是这样的,但是需要注意的是,解除锁的操作尽量写在finally代码块中,这样可以避免因为程序加锁后代码执行时抛出异常,导致锁无法释放,而产生期望之外的程序异常。
使用场景:任务中执行的读操作远远大于写操作,这时可以考虑读写分离锁。

上述的优化操作依然是加锁,锁在java处理并发任务这一块,功不可没,但是加锁必然带来上下文切换和重新调度时的性能开销,volatile虽然可以实现内存上的可见行,但是并不能操作的原子性,那么有没有办法不加锁还能保证原子性呢?

原子类-Atomic

JDK中提供了一种特殊的原子类,比如AtomicInteger、AtomicBoolean等等,它们是使用CAS算法实现的线程安全的无锁类,专门用于多线程并发操作。
CAS全称Compare And Swap(比较和交换),作为一个Android程序员,我们一般只需要知道Java从硬件上保证了比较-交换操作的原子性,关于它的内部细节,不需要过分深究。
使用场景:当我们在使用java基本数据类型,一些更新、累加操作需要保证原子性时。
注意:当我们需要对一些变量做一些复杂的操作,而这些操作原子类中并没有提供时,我们应该首先考虑使用锁而不是原子类。
我们使用原子类来改写上面的例子

    private AtomicInteger value = new AtomicInteger(0);

    public int getValue() {
        //累加
        return value.incrementAndGet();
    }

    public void setValue(int value) {
        //设定新的值
        this.value.getAndSet(value);
    }

线程池

说完了锁的优化之后,我们在回过头来一下,探讨以下线程的优化。
在Android开发中我们鼓励甚至要求程序员必须使用线程池来创建新的线程。前一篇文章中介绍了6种新建线程的方式,为什么鼓励使用线程池来新建线程?
原因在于线程池有以下几个优点:
1.重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销。
2.能有效控制线程的最大并发数量,避免大量线程之间因互相抢占cpu而导致的阻塞现象。
3.能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。

在Android中线程池都是通过直接或间接配置ThreadPoolExecutor来实现的,下面简单介绍一下ThreadPoolExecutor

        //核心线程的数量
        int threads = 1;
        //最大线程数
        int maximumPoolSize = 10;
        //非核心线程的闲置超时时间
        long keepAliveTime = 100L;
        //超时单位
        TimeUnit unit = TimeUnit.SECONDS;
        //线程池中阻塞任务队列
        LinkedBlockingDeque<Runnable> deque = new LinkedBlockingDeque<>();
        //线程工厂
        ThreadFactory factory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "线程的名字");
            }
        };
        //线程池
        ExecutorService executorService = new ThreadPoolExecutor(threads,
                maximumPoolSize, keepAliveTime, unit, deque, factory);

        //向线程池中传入一个runnable
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //do something
            }
        });

核心线程:即使处于闲置状态,系统也不会销毁的线程。
maximumPoolSize:最大线程数,线程池所能容纳的最大线程数,当活动线程达到这个数值后,后续的任务会阻塞。
keepAliveTime:非核心线程闲置的超时时间:超过这个时长,非核心的线程会被回收。当allowThreadTimeOut属性为true时,这个时间也会作用于核心线程。
workQueue:线程池中阻塞任务队列,通过excute方法提交的runnable对象会存储在这个参数中。
threadFactory:线程工厂,用于初始化统一规格的线程。

线程池在运行时遵守以下的规则
1.如果线程池中线程未达到核心线程的数量,那么会直接启用一个核心的线程来执行任务。
2.如果线程池中的任务达到或者超过核心线程的数量,那么任务会被插入到任务队列中等待执行。
如果步骤2中无法将任务插入到任务队列中(任务队列已满),此时如果线程池中线程数量未达到线程池规定的最大值,那么会立即启动一个非核心线程来执行任务。如果线程数已经达到了线程池中规定的最大值,为抛出异常rejectedExecutionException。

根据不同的任务配置线程池

在实际的开发中,我们需要根据不同的任务类型,配置合适的线程池,这些任务类型大致有以下两种。
CPU密集型操作:核心线程应该尽量少一些,如CPU数量+1(保证核心线程的执行积极度是一样的)
I/O密集型操作:IO操作不占用cpu,线程数量可以多一些,但也不能过多,否则线程切换带来的开销又会影响到性能。
总结起来就是线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

常见的线程池

Java中为我们配置多种常用的线程池,根据执行任务的不同,我们可以直接使用Executors创建出不同的线程池,而不需要再做配置。
1.FixedThreadPool
线程数量固定的线程池,核心线程数量=最大线程数量,并且只有核心线程,当线程处于空闲状态时,它们并不会被回收,除非线程池关闭。线程池队列无限大。
作用:快速响应外界的请求

        //线程池
        ExecutorService executorService;
        //核心线程的数量
        int threads = 1;
        //重现方法1
        executorService = Executors.newFixedThreadPool(1);
        //重载方法2
        executorService = Executors.newFixedThreadPool(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "线程的名字");
            }
        });
        //像线程池传入一个runnable
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //do something
            }
        });

2.CachedThreadPool
线程数量无限大闲置的线程池,并且没有存储任务的队列,线程超时时间为60秒。这意味它会立即处理所有加入进来的任务,在没有任务时,线程会因为超时而被回收,这时它是几乎不占用任何系统资源的。
作用:适合处理高并发,且耗时较少的任务。

        //线程池
        ExecutorService executorService;
        executorService = Executors.newCachedThreadPool(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "线程的名字");
            }
        });
        //像线程池传入一个runnable
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //do something
            }
        });

3.ScheduledThreadPool
核心线程数固定,非核心线程数无限大,非核心线程超时时间10秒。
作用:用于执行定时任务和具有固定周期的重复任务。

//核心线程的数量
        int threads = 1;
        //定时
        long delay = 2000L;
        //延迟
        long initDelay = 1000L;
        //线程工厂
        ThreadFactory factory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "线程的名字");
            }
        };
        //线程池
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(threads, factory);
        //方法1:不延迟直接定时执行
        executorService.schedule(new Runnable() {
            @Override
            public void run() {
                //需要定时执行的任务
            }
        }, delay, TimeUnit.SECONDS);
        //方法2:延迟后再定时执行
        executorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                //
            }
        }, initDelay, delay, TimeUnit.SECONDS);

4.SingleThreadExecutor
线程池中只有一个核心线程,线程池队列无限大
作用:统一外界所有的任务到一个线程中,使这些任务之间不需要处理线程同步的问题。

        //线程池
        ExecutorService executorService;
        //重现方法1
        executorService = Executors.newSingleThreadExecutor();
        //重载方法2
        executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "线程的名字");
            }
        });
        //像线程池传入一个runnable
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //do something
            }
        });

注意上述所说的无限大实际上是指Java的一个常数Integer.MAX_VALUE,它等于2的31次方-1,我们可以把它近似看作无穷大。

后台任务的选择

在Android开发中会经常遇到不同的后台任务,根据不同的任务类型,我们需要选择不同的实现方式,下面说说一些简单的判断场景。

当一个后台任务只运行在后台且不会回到前台或不会与UI发生交互时,考虑使用线程池。
当一个后台任务在后台短期执行后需要返回前台的,考虑使用AyncTask或HandlerThread
以上情况并不是绝对的,有时候甚至需要结合service、intentService等组件一起,才能完成后台任务,说到底适合当前项目的,才是最好的。

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

推荐阅读更多精彩内容