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

目录
相关文章
|
2月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
265 4
|
1月前
|
安全 Java 程序员
ArrayList vs Vector:一场线程安全与性能优化的世纪之争!
在 Java 面试中,ArrayList 和 Vector 是高频考点,但很多人容易混淆。本文通过10分钟深入解析它们的区别,帮助你快速掌握性能、线程安全性、扩容机制等核心知识,让你轻松应对面试题目,提升自信!
57 18
|
1月前
|
数据采集 机器学习/深度学习 前端开发
PHP爬虫性能优化:从多线程到连接池的实现
本文介绍了一种通过多线程技术和连接池优化PHP爬虫性能的方法,以新浪投诉平台为例,详细展示了如何提高数据采集效率和稳定性,解决了传统单线程爬虫效率低下的问题。
PHP爬虫性能优化:从多线程到连接池的实现
|
29天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
63 1
|
1月前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
1月前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
1月前
|
网络协议 Linux Android开发
深入探索Android系统架构与性能优化
本文旨在为读者提供一个全面的视角,以理解Android系统的架构及其关键组件。我们将探讨Android的发展历程、核心特性以及如何通过有效的策略来提升应用的性能和用户体验。本文不包含常规的技术细节,而是聚焦于系统架构层面的深入分析,以及针对开发者的实际优化建议。
70 1
|
2月前
|
API Android开发 iOS开发
深入探索Android与iOS的多线程编程差异
在移动应用开发领域,多线程编程是提高应用性能和响应性的关键。本文将对比分析Android和iOS两大平台在多线程处理上的不同实现机制,探讨它们各自的优势与局限性,并通过实例展示如何在这两个平台上进行有效的多线程编程。通过深入了解这些差异,开发者可以更好地选择适合自己项目需求的技术和策略,从而优化应用的性能和用户体验。
|
2月前
|
Android开发 开发者
Android性能优化——内存管理的艺术
Android性能优化——内存管理的艺术
|
2月前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
51 5

热门文章

最新文章

相关实验场景

更多