Android 多线程并发优化实现

程序员Android

共 8951字,需浏览 18分钟

 · 2024-04-10

cd2c475b73344ae10ca8720147502c29.webp

和你一起终身学 习,这里是程序员Android

经典好文推荐,通过阅读本文,您将收获以下知识点:

一、Thread 使用
二、Android Thread
三.线程优先级

一、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.Thread 同步

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

(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 Thread

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

8f57e5381dbbdfd044a7e9bfa7404334.webp

程序员Android转于网络

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

1.AsyncTask

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

  • 使用AsyncTask需要注意的问题?

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

cbb79df9849db0ac2d8a8948c18857fc.webp

程序员Android转于网络

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

(2).AsyncTask对正在执行的任务不具备取消的功能,所以我们要在任务代码中添加取消的逻辑(和上面Thread类似)

(3).AsyncTask使用不当会导致内存泄漏(可以参考内存泄漏一章)

2.HandlerThread

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

7958f29148330bfee38899cb3c6c0cf2.webp

程序员Android转于网络

我们先来看下源码:

        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不受主页面生命周期影响的特点。

1acf55d7ad497ae33e9aa7908567ed9b.webp

程序员Android转于网络

使用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帮助类来帮助我们简化实现线程池。

e674b1f97f2dbe37289877cf7d3df330.webp

程序员Android转于网络

使用线程池需要特别注意同时并发线程数量的控制,理论上来说,我们可以设置任意你想要的并发数量,但是这样做非常的不好。因为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的程序本身的优先级就更高,理应得到更多的执行时间。

a1f739135ed165a7017725265b6d6d46.webp

程序员Android转于网络

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

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

a637e27c17bd639f2daac75f3f777126.webp

程序员Android转于网络

大多数情况下,新创建的线程优先级会被设置为默认的0,主线程设置为0的时候,新创建的线程还可以利用THREAD_PRIORITY_LESS_FAVORABLE或者THREAD_PRIORITY_MORE_FAVORABLE来控制线程的优先级。

参考链接:https://www.jianshu.com/p/2795aef8980d

至此,本篇已结束。转载网络的文章,小编觉得很优秀,欢迎点击阅读原文,支持原创作者,如有侵权,恳请联系小编删除,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!

浏览 3
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报