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

目录
相关文章
|
4月前
|
数据采集 存储 JSON
Python爬取知乎评论:多线程与异步爬虫的性能优化
Python爬取知乎评论:多线程与异步爬虫的性能优化
|
4月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
200 0
|
5月前
|
数据库
鸿蒙5开发宝藏案例分享---跨线程性能优化指南
本文深入探讨鸿蒙系统跨线程序列化性能优化,借助DevEco Profiler工具定位序列化瓶颈。通过Sendable接口改造、数据瘦身等方法,将5万本书对象的序列化耗时从260ms+降至&lt;8ms,甚至&lt;1ms。总结避坑经验,建议常态化使用Profiler检测,避免传递大对象,提升多线程开发效率。
|
4月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
5月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
363 5
|
9月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
348 20
|
9月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
11月前
|
安全 Java 程序员
ArrayList vs Vector:一场线程安全与性能优化的世纪之争!
在 Java 面试中,ArrayList 和 Vector 是高频考点,但很多人容易混淆。本文通过10分钟深入解析它们的区别,帮助你快速掌握性能、线程安全性、扩容机制等核心知识,让你轻松应对面试题目,提升自信!
262 18
|
11月前
|
数据采集 机器学习/深度学习 前端开发
PHP爬虫性能优化:从多线程到连接池的实现
本文介绍了一种通过多线程技术和连接池优化PHP爬虫性能的方法,以新浪投诉平台为例,详细展示了如何提高数据采集效率和稳定性,解决了传统单线程爬虫效率低下的问题。
322 2
PHP爬虫性能优化:从多线程到连接池的实现
|
11月前
|
网络协议 Linux Android开发
深入探索Android系统架构与性能优化
本文旨在为读者提供一个全面的视角,以理解Android系统的架构及其关键组件。我们将探讨Android的发展历程、核心特性以及如何通过有效的策略来提升应用的性能和用户体验。本文不包含常规的技术细节,而是聚焦于系统架构层面的深入分析,以及针对开发者的实际优化建议。
333 21

热门文章

最新文章

下一篇
oss云网关配置