Android 并发/多线程 的基础与应用

时间:2022-11-05 21:13:24

本篇文章主要目的为总结 覆盖80%场景的20% Android端并发所需基础知识和应用。
Android 端应用主要使用 Java 语言开发,所以基础与 Java 的并发基础基本一样,深入了解推荐细读《Java并发编程实践》。应用部分就会掺杂 Android 的东西了。

并发与线程、线程与线程不安全

讨论并发其实就是在讨论多线程。
而并发这个编程主题长久地被人拎出来讨论的原因,被前人们总结成一个词 “线程安全”。
没接触过多线程编程的小白,很容易会掉进像下面这样的不安全陷阱:
读取-修改-写入 的中途打断;
单例模式意外怀孕(多个线程同时初始化获取第一个单例);
同一个变量,在不同线程读取到的值不一样。
更多的例子不写了,我总结为:
1.非原子性状态变更的中途打断;2.多线程对同一状态的观察失去同步。
这两点,也对应着后文提到的并发编程核心问题和核心关键词。

线程世界安全局核心*物——Volatile\Synchronize

Volatil (“曼哈顿博士,直接观察基本粒子”),解决可见性问题

说到可见性问题,首先要理解几个概念。

物理内存+高速缓存

计算机运行一个程序的时候,变量最终还是需要存储在一个物理位置的。这就导致了每次对变量的操作,比如 i++,就需要 T(i++ 一次循环) = T(读IO) + T(+1运算)+T(写IO)。运算往往很快,拖后腿的是IO。
为此,物理层加入了一个高速缓存层。这个层读写速度飞快,在初始化的时候从内存拷一次变量值到高速缓存,之后程序再改这个变量就不用再等乌龟内存层了,直接跟高速缓存打交道,高速缓存先处理了再跟乌龟慢慢交涉。

Jvm 内存+线程本地内存副本

当理解了 物理内存与高速缓存 ,问题就好说了。我告诉你,JVM 也用了类似的设计: 物理内存–>JVM内存,高速缓存–>本地内存副本。
物理内存+高速缓存 服务与程序;Jvm 内存+线程本地内存副本 服务于线程。
设计的维度和颗粒度不一样,但理念是相通的。

可见性

可见性问题就是,两个线程都分别从主内存拷贝了一个变量A副本,然后各自修改的就其实都是自己线程工作空间上的那个副本,对主内存的变量A没有影响,对其他线程的那个变量A更没有影响。
Volatile 就可以让他们修改后同步到主内存,读取时主动从主内存读取。还可以禁止指令重排。
通过汇编指令“lock”前缀做到,具体的原理可细读《深入学习Java虚拟机》。

Synchronize(“游侠锁罗”),同时解决 可见性、原子性 问题

当它用来修饰一个方法或者一个代码块时,保证在同一时刻最多只有一个线程执行该段代码。
通过使用内置锁,来实现对变量的同步操作,进而实现了对变量擦偶哦的【原子性】和其他线程对变量的【可见性】。

内置锁

Java 中每一个对象都有一个与之关联的锁,称为内置锁。
个人将内置锁分为 实例锁 和 类锁
Synchronize 修饰非静态方法时,用的就是该方法实例的内置锁,也就是this。修饰静态方法时用的就是类锁。

需要多把锁的时候

随便 new 个 object 就可以当锁。也有显示的锁 Lock lock = …;(形形式式的各种加了特效的显式锁)

锁的持有者是线程,锁可重入

持有对应的锁,就可以进入所有被锁的地方,无论这个地方在哪里,在子类还是在父类等。

线程世界安全局马仔

ConcurrentHashMap
CopyonWriteArrayList
LinkedBlockingQueue

线程世界的资源局——线程池、 HabdlerThread、IntentService

辣鸡 new Thread

Runnable作为匿名内部类还持有了外部类的引用,在线程退出之前,该引用会一直存在,阻碍外部类对象被GC回收,在一段时间内造成内存泄漏。
1.每次都需要new Thread,新建对象性能差。
2.线程缺乏统一管理,可能无限制新建线程,相互之间竞争,极可能占用过多系统资源导致死机或OOM。
3.缺乏更多功能,如定时执行、定期执行、线程中断

线程池


final ExecutorService executor = Executors.newCachedThreadPool();
        executor.execute(new Runnable() {
            @Override
            public void run() {
                final Bitmap bitmap = downloadImage(beautyUrl);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mBeautyImageView.setImageBitmap(bitmap);
                    }
                });
                executor.shutdown();
            }
        });

引入的好处:
1.提升性能,创建和消耗对象费时费CPU资源。
2.防止内存过度消耗,控制活动线程的数量,防止并发线程过多。
如何选择?
使用Excutors.newCashedThreadpool通常是个不错的选择,因为它不需要配置,并且一般情况下都能够正确地完成工作。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了!在缓存的线程池中,被提交的任务没有排成队列。而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载的太重,以致他所有的CPU都完全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。因此在大负载的产品服务器中,最好使用Excutors.newFixedThreadPool,它为你提供了一个包含固定线程数目的线程池,或者为了最大的限度地控制它,就直接使用ThreadPoolExcutor类。

HabdlerThread——轻量级任务处理器

HandlerThread是Android中提供特殊的线程类,使用这个类我们可以轻松创建一个带有Looper的线程,同时利用Looper我们可以结合Handler实现任务的控制与调度。以Handler的post方法为例,我们可以封装一个轻量级的任务处理器。

private Handler mHandler;
private LightTaskManager() {
    HandlerThread workerThread = new HandlerThread("LightTaskThread");
    workerThread.start();
    mHandler = new Handler(workerThread.getLooper());
}

public void post(Runnable run) {
    mHandler.post(run);
}

public void postAtFrontOfQueue(Runnable runnable) {
    mHandler.postAtFrontOfQueue(runnable);
}

public void postDelayed(Runnable runnable, long delay) {
    mHandler.postDelayed(runnable, delay);
}

public void postAtTime(Runnable runnable, long time) {
    mHandler.postAtTime(runnable, time);
}

在本例中,我们可以按照如下规则提交任务

post 提交优先级一般的任务
postAtFrontOfQueue 将优先级较高的任务加入到队列前端
postAtTime 指定时间提交任务
postDelayed 延后提交优先级较低的任务
上面的轻量级任务处理器利用HandlerThread的单一线程 + 任务队列的形式,可以处理类似本地IO(文件或数据库读取)的轻量级任务。在具体的处理场景下,可以参考如下做法:

对于本地IO读取,并显示到界面,建议使用postAtFrontOfQueue
对于本地IO写入,不需要通知界面,建议使用postDelayed
一般操作,可以使用post。
(实验怎么回调)
(强化参考:http://blog.csdn.net/javazejian/article/details/52426353
过handlerThread.quit()或者quitSafely()使线程结束自己的生命周期。

IntentService

耗时逻辑应放在onHandleIntent(Intent intent)的方法体里,它同样有着退出启动它的Activity后不会被系统杀死的特点,而且当任务执行完后会自动停止,无须手动去终止它。例如在APP里我们要实现一个下载功能,当退出页面后下载不会被中断,那么这时候IntentService就是一个不错的选择了。
IntentService背后其实也有一个HandlerThread来串行的处理Message Queue,从IntentService的onCreate方法可以看出。跟我们的轻量任务处理器类似。

通讯: 工作线程干活后怎么在必要的时候切回主线程?

Future,FutureTask和Callable

http://blog.csdn.net/nugongahou110/article/details/49967495
这个还没深入研究过。

AsyncTask

恕我直言,一坨一坨的��一样好烦人。只能从UIThread发出, 也有隐式的持有外部类对象引用的问题,需要特别注意防止出现意外的内存泄漏。在不同的系统版本上串行与并行的执行行为不一致。反正我是不爱用。

Thread+Handler+MessageQueue+Looper

(!todo 原理解析,案例)
内置UI线程操作底层也是利用了这个原理。

内置UI线程操作

Activity.runOnUiThread(Runnable)
View.post(Runnable) ,View.postDelayed(Runnable, long)
Handler(UI线程的)的postDelayed
Boardcast

线程优先级调整

在Android应用中,将耗时任务放入异步线程是一个不错的选择,那么为异步线程调整应有的优先级则是一件锦上添花的事情。众所周知,线程的并行通过CPU的时间片切换实现,对线程优先级调整,最主要的策略就是降低异步线程的优先级,从而使得主线程获得更多的CPU资源。
Android中的线程优先级和Linux系统进程优先级有些类似,其值都是从-20至19。其中Android中,开发者可以控制的优先级有:
* THREAD_PRIORITY_DEFAULT,默认的线程优先级,值为0
* THREAD_PRIORITY_LOWEST,最低的线程级别,值为19
* THREAD_PRIORITY_BACKGROUND 后台线程建议设置这个优先级,值为10
* THREAD_PRIORITY_MORE_FAVORABLE 相对
* THREAD_PRIORITY_DEFAULT稍微优先,值为-1
* THREAD_PRIORITY_LESS_FAVORABLE 相对
* THREAD_PRIORITY_DEFAULT稍微落后一些,值为1
为线程设置优先级也比较简单,通用的做法是在run方法体的开始部分加入下列代码

new Thread(new Runnable() {
  @Override
  public void run() {
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
  }
}).start();

在我们决定新启一个线程执行任务的时候,首先要问自己这个任务在完成时间上是否重要到要和UI线程争夺CPU资源。如果不是,降低线程优先级将其归于background group,如果是,则需要进一步的profile看这个线程是否造成UI线程的卡顿。
通常设置优先级的规则如下:
* 一般的工作者线程,设置成THREAD_PRIORITY_BACKGROUND
* 对于优先级很低的线程,可以设置THREAD_PRIORITY_LOWEST
* 其他特殊需求,视业务应用具体的优先级

其他

SharePreference是否线程安全?多线程操作是否存在隐患?

线程安全。可以用作线程间少量数据共享,但是不能用在进程间。
进程不安全。虽然在API Level>=11即Android 3.0可以通过Context.MODE_MULTI_PROCESS属性来实现SharedPreferences多进程共享。但每个进程操作的SharedPreferences其实都是一个单独的实例, 也不能通过锁来解决,这导致了多进程间通过SharedPreferences来共享数据是不安全的,这个问题只能通过多进程间其它的通信方式或者是在确保不会同时操作SharedPreferences数据的前提下使用SharedPreferences来解决。
Use SharedPreferences on multi-process mode
请不要滥用SharedPreference

android中数据库支持多线程吗?

sqlite支持多线程。但是安卓在封装sqlite的java接口的时候加了表锁,所以会造成多个线程读一个表的时候阻塞。这也是为什么读写数据库的操作要放到线程里做,防止ANR。

默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。 但是,如果您发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。

Final Tips

“同构任务”和“并行”更配哦��

异步时要注意Activity的生命周期

异步时最容易出错的就是忽略Activity的生命周期。比如,当异步执行完成了,Activity却退出了前台,或者已经结束,如果异步完成时要操作UI,那么这种情况下肯定会报错,具体的错误取决于场景。这个问题的解法就是在异步操作完成后要用Activity.isFinishing()来判断下Activity是否还是alive的。或者设置一个变量来查看Activity是否还在前台。
另外,即使异步操作中不涉及UI,那么当Activity转入后台,或者退出时,也要及时的终止工作线程,否则也会造成Activity的对象无法及时销毁而最终导致内存泄露。这个问题需要在设计异步task时把可取消考虑进去,当Activity退出前台时发送消息给线程,让其终止执行。对于常见的费时操作,比如IO,网络,复杂计算等在都要考虑取消,每一个小步骤执行前都要判断取消标志位,以及时终止操作。通常这需要在Activity中持有任务的引用,或者使用Executors来管理任务,或者有一个类似的对象来管理异步任务,当Activity退出时,来终止任务。或者使用EventBus这类工具来降低耦合。