深入了解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等组件一起,才能完成后台任务,说到底适合当前项目的,才是最好的。

目录
相关文章
|
18天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
18天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第9天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细解析Java中的同步机制,包括synchronized关键字、Lock接口以及并发集合等,并探讨它们如何影响程序的性能。此外,我们还将讨论Java内存模型,以及它如何影响并发程序的行为。最后,我们将提供一些实用的并发编程技巧和最佳实践,帮助开发者编写出既线程安全又高效的Java程序。
23 3
|
25天前
|
缓存 监控 Android开发
安卓应用性能优化的实用策略
【4月更文挑战第2天】 在竞争激烈的应用市场中,一款应用的性能直接影响用户体验和市场表现。本文针对安卓平台,深入探讨了性能优化的关键要素,包括内存管理、代码效率、UI渲染和电池使用效率。通过分析常见的性能瓶颈,并提供针对性的解决策略,旨在帮助开发者构建更加流畅、高效的安卓应用。
|
17天前
|
算法 Java 开发者
Java中的多线程编程:概念、实现与性能优化
【4月更文挑战第9天】在Java编程中,多线程是一种强大的工具,它允许开发者创建并发执行的程序,提高系统的响应性和吞吐量。本文将深入探讨Java多线程的核心概念,包括线程的生命周期、线程同步机制以及线程池的使用。接着,我们将展示如何通过继承Thread类和实现Runnable接口来创建线程,并讨论各自的优缺点。此外,文章还将介绍高级主题,如死锁的预防、避免和检测,以及如何使用并发集合和原子变量来提高多线程程序的性能和安全性。最后,我们将提供一些实用的性能优化技巧,帮助开发者编写出更高效、更稳定的多线程应用程序。
|
16天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
2天前
|
安全 算法 Java
JavaSE&多线程&线程池
JavaSE&多线程&线程池
17 7
|
3天前
|
存储 缓存 NoSQL
为什么Redis使用单线程 性能会优于多线程?
在计算机领域,性能一直都是一个关键的话题。无论是应用开发还是系统优化,我们都需要关注如何在有限的资源下,实现最大程度的性能提升。Redis,作为一款高性能的开源内存数据库,因其出色的单线程性能而备受瞩目。那么,为什么Redis使用单线程性能会优于多线程呢?
15 1
|
12天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。
|
14天前
|
Java API 调度
安卓多线程和并发处理:提高应用效率
【4月更文挑战第13天】本文探讨了安卓应用中多线程和并发处理的优化方法,包括使用Thread、AsyncTask、Loader、IntentService、JobScheduler、WorkManager以及线程池。此外,还介绍了RxJava和Kotlin协程作为异步编程工具。理解并恰当运用这些技术能提升应用效率,避免UI卡顿,确保良好用户体验。随着安卓技术发展,更高级的异步处理工具将助力开发者构建高性能应用。
|
17天前
|
监控 安全 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第10天】 在Java开发中,并发编程是提升应用性能和响应能力的关键手段。然而,线程安全问题和性能调优常常成为开发者面临的挑战。本文将通过分析Java并发模型的核心原理,探讨如何平衡线程安全与系统性能。我们将介绍关键的同步机制,包括synchronized关键字、显式锁(Lock)以及并发集合等,并讨论它们在不同场景下的优势与局限。同时,文章将提供实用的代码示例和性能测试方法,帮助开发者在保证线程安全的前提下,实现高效的并发处理。

热门文章

最新文章

相关实验场景

更多