这篇是Glide源码解读系列的第二篇,主要分析Glide图片加载框架中对并发操作的处理。阅读本篇之前,首先得对Glide流程很了解,不了解Glide图片加载流程的建议先阅读第一篇 Glide源码解读(一) : 主流程分析 。
Java并发
分析Glide源码中并发处理之前先说说Java的并发,并发基础很扎实的同学可以跳过直接看分析。
并发编程,从程序设计的角度来说,是希望通过某些机制让计算机可以在一个时间段内,执行多个任务。从计算机 CPU 硬件层面来说,是一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。从调度算法角度来说,当任务数量多于 CPU 的核数时,并发编程能够通过操作系统的任务调度算法,实现多个任务一起执行。
并发编程有很多优点,可以能提高程序的运行性能,充分利用系统的处理能力,可以提高系统的资源利用率等等。
优点越突出缺点也是越明显,在并发中很容易导致的就是安全性问题,在单线程系统上正常运行的代码,在多线程环境中可能会出现意料之外的结果。
如何处理并发安全性问题
1.互斥同步
互斥同步又称阻塞同步,是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用,常用的有synchronized、ReentrantLock。
2.非互斥同步
互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,属于一种悲观的并发策略,而非互斥同步属于乐观并发策略,实现不再需要把线程阻塞挂起,常用的有使用CAS实现的AtomicInteger等。
3.无同步方案
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题,可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。
如果是一个线程写,多个线程读的情况下,可以使用volatile,volatile保证了可见性和禁止指令重排序。
对于实现原理本文就不深究了,有兴趣的可以自己找资料学习下。
总结一下 , Java对并发安全的处理主要有:volatile ,synchronized ,ReentrantLock, CAS。实际上glide源码中基本也都是用到这些来保证并发安全性的,好了接下来就开始分析Glide源码。
Gldie 初始化并发的处理
先分析下glide初始化是怎么保证并发安全的。
private static volatile Glide glide;
public static Glide get(@NonNull Context context) {
if (glide == null) {
synchronized (Glide.class) {
if (glide == null) {
checkAndInitializeGlide(context, annotationGeneratedModule);
}
}
}
return glide;
}
这是一种典型的 双重校验锁 实现的单例模式。volatile这里用于禁止指令重排序,先说说 new 对象大致过程:
1.先判断对象对应的Class是否已经加载,如果没有加载就先加载Class
2.确定对象占用大小,分配堆内存
3.给所有实例变量赋默认值
4.初始化,执行对象构造方法
5.给引用赋值,将堆区对象的地址赋值给引用
这一系列过程会执行很多指令,volatile关键字可以保证这些指令按顺序执行。如果没有volatile关键字,有可能会获取到一个没有初始化的glide对象。
synchronized是互斥锁,可以保证synchronized锁住的这一段代码,并发情况下只能被一个线程执行,所以new只能被执行一次,全局只有一个glide实例对象。
EngineJob 并发的处理
接下来分析EngineJob 类中对并发的处理,熟悉Glide源码的都知道EngineJob 这个类非常关键,类似于中间件的作用。用于启动DecodeJob 去下载/解码/转码获取可以显示的图片, 并将最终可以显示的图片回调给Request中的ImageView显示,这些过程涉及到多个线程同时操作这个类,很容易出现并发导致数据不安全性的问题。
在EngineJob 类中搜索synchronized。
可以看到synchronized关键字出现了16次。
CAS实现的AtomicInteger出现了一次即pendingCallbacks ,默认值为0。
addCallback方法synchronized的作用
先分析addCallback方法上synchronized的作用,因为源码调用这个方法最先出现。
key为EngineKey,如下。
根据EngineKey获取EngineJob , 如果model ,width,height等数据一样,则获取到的就是同一个EngineJob ,其实这种情况很多。跟进EngineJob #addCallback方法。
synchronized void addCallback(final ResourceCallback cb, Executor callbackExecutor) {
stateVerifier.throwIfRecycled();
//1. cbs.add,添加回调
cbs.add(cb, callbackExecutor);
if (hasResource) {
incrementPendingCallbacks(1);
callbackExecutor.execute(new CallResourceReady(cb));
} else if (hasLoadFailed) {
incrementPendingCallbacks(1);
callbackExecutor.execute(new CallLoadFailed(cb));
} else {
Preconditions.checkArgument(!isCancelled, "Cannot add callbacks to a cancelled EngineJob");
}
}
第一个作用:保证 cbs.add,添加图片显示回调的安全。
cbs.add ,使用arraylist作为容器 ,最终会调用arraylist.add ,arraylist是并发不安全的容器,高并发情况下很容易导致数据丢失,此处数据为图片显示回调,所以不加synchronized会导致高并发下图片无法显示出来,因为高并发下回调可能被覆盖丢失了。
第二个作用:保证其他线程添加回调的安全性。
EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
if (current != null) {
current.addCallback(cb, callbackExecutor);
return new LoadStatus(cb, current);
}
假设此时是 current!=null的情况,这种情况对应的此时已经有个EngineJob任务正在执行,所以我们就添加回调等待第一个任务执行完回调被调用就行了。
假设正在执行那个EngineJob任务还没有执行到notifyCallbacksOfResult 方法 里的synchronized 代码块。
void notifyCallbacksOfResult() {
ResourceCallbacksAndExecutors copy;
Key localKey;
EngineResource<?> localResource;
synchronized (this) {
stateVerifier.throwIfRecycled();
if (isCancelled) {
resource.recycle();
release();
return;
} else if (cbs.isEmpty()) {
throw new IllegalStateException("Received a resource without any callbacks to notify");
} else if (hasResource) {
throw new IllegalStateException("Already have resource");
}
engineResource = engineResourceFactory.build(resource, isCacheable, key, resourceListener);
hasResource = true;
copy = cbs.copy();
incrementPendingCallbacks(copy.size() + 1);
localKey = key;
localResource = engineResource;
}
所以此时第一个执行EngineJob 任务的线程,如果执行到synchronized的时候会被阻塞住,直到第二个线程的EngineJob #addCallback执行完才被唤醒。如果addCallback中不加synchronized ,会导致一个线程在copy一个线程在add回调的情况同时发生,如果第二个线程的copy比第一个线程的add快, 会导致copy的回调中并没有第二个线程的回调,所以第二个线程无法把加载好的图片显示出来,synchronized 就是为了防止这种情况发生。
removeCallback方法synchronized 作用
调用cancel取消图片加载的时候会调用removeCallback 方法
removeCallback中又是加了一把锁
synchronized void removeCallback(ResourceCallback cb) {
stateVerifier.throwIfRecycled();
cbs.remove(cb);
if (cbs.isEmpty()) {
cancel();
boolean isFinishedRunning = hasResource || hasLoadFailed;
if (isFinishedRunning && pendingCallbacks.get() == 0) {
release();
}
}
}
void cancel() {
if (isDone()) {
return;
}
isCancelled = true;
decodeJob.cancel();
engineJobListener.onEngineJobCancelled(this, key);
}
notifyCallbacksOfResult 会在图片加载成功回调,这个方法在线程池中调用,和cancel不在同一线程。
removeCallback 方法如果不加synchronized 两个线程并发执行,可能会导致cancel不成功的情况发生,虽然cancel了但是图片还是可能会显示出来。
release 方法synchronized的作用
先看relase方法做了什么
把成员变量值置为空或者是默认值,然后放入对象池中。 release 在以下几种情况会被调用:
- 执行完图片显示回调之后 ,发现没有待处理的回调。即pendingCallbacks =0时
- 调用notifyCallbacksOfResult 方法 当 isCancelled = true 时
- cancel 取消图片加载,hasResource || hasLoadFailed 为true && pendingCallbacks = 0 时
如果不加synchronized 会导致什么问题?
假设此时cancel 后relase方法刚被调用,如果不加synchronized 并发很高情况下会导致回调图片资源为null值情况发生。
接下来看看DecodeJob 对并发的处理。
DecodeJob 并发的处理
按理说这个只在单线程中使用,并发应该很少,在类中搜索synchronized,CAS。
果不其然,只有静态内部类ReleaseManager里synchronized出现了四次。
我们来看下这四种情况,分别在什么地方被调用,为什么会出现并发的情况。
- ReleaseManager#release 方法
在EngineJob#release中被调用
//EngineJob#release
private synchronized void release() {
decodeJob.release(/*isRemovedFromQueue=*/ false);
}
- ReleaseManager#onEncodeComplete方法
在DecodeJob#notifyEncodeAndRelease中被调用 ,notifyEncodeAndRelease在解码之后被调用
private void notifyEncodeAndRelease(
Resource<R> resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
//回调图片给EngineJob
notifyComplete(result, dataSource, isLoadedFromAlternateCacheKey);
stage = Stage.ENCODE;
try {
//编码,缓存解码之后的图片
deferredEncodeManager.encode(diskCacheProvider, options);
}
onEncodeComplete();
} finally {
GlideTrace.endSection();
}
}
- ReleaseManager#onFailed方法
在图片加载失败notifyFailed方法中被调用。
private void notifyFailed() {
callback.onLoadFailed(e);
onLoadFailed();
}
private void onLoadFailed() {
if (releaseManager.onFailed()) {
releaseInternal();
}
}
notifyFailed 又在run 中当Cancel =true 被调用,在runGenerators 中当stage == Stage.FINISHED || isCancelled 被调用。
- ReleaseManager#reset方法
reset 方法只有在DecodeJob#releaseInternal 中被调用,用来重置状态。
private void releaseInternal() {
releaseManager.reset();
}
好了总结一下,ReleaseManager这个类用来给DecodeJob判断是否可以执行回收操作。在取消 ,加载成功,加载失败会被调用。那为什么要加上synchronized? 既然加上了肯定就是有它的意义的,我们还是用假设法来大胆假设一下。 假设此时在主线程调用了cancel用来取消图片加载操作,EngineJob因为取消release方法被主线程调用。而此时图片因为网络或者其他原因图片加载失败,DecodeJob#notifyFailed会被子线程调用。此时如果ReleaseManager不加synchronized的话,cancel 执行的release和notifyFailed并发执行,关键代码如下。
cancel 执行相关代码。
void release(boolean isRemovedFromQueue) {
if (releaseManager.release(isRemovedFromQueue)) {
releaseInternal();
}
}
synchronized boolean release(boolean isRemovedFromQueue) {
isReleased = true;
return isComplete(isRemovedFromQueue);
}
private boolean isComplete(boolean isRemovedFromQueue) {
return (isFailed || isRemovedFromQueue || isEncodeComplete) && isReleased;
}
private void releaseInternal() {
glideContext = null;
signature = null;
options = null;
priority = null;
loadKey = null;
//省略大部分资源回收相关代码
callback = null;
pool.release(this);
}
notifyFailed 执行的代码
private void notifyFailed() {
callback.onLoadFailed(e);
onLoadFailed();
}
private void onLoadFailed() {
if (releaseManager.onFailed()) {
releaseInternal();
}
}
synchronized boolean onFailed() {
isFailed = true;
return isComplete(false /*isRemovedFromQueue*/);
}
private void releaseInternal() {
glideContext = null;
signature = null;
options = null;
priority = null;
loadKey = null;
//省略大部分资源回收相关代码
callback = null;
pool.release(this);
}
因为并发操作 ,提前执行releaseInternal导致DecodeJob提前被回收导致失败回调失效,也有可能releaseInternal被多次调用导致同一对象被多次放入对象池 。
总结
本文先分析了Glide初始化因为并发可能导致的问题,然后再分析EngineJob,DecodeJob 中对并发的处理,如果不做并发处理很容易出现问题。实际上Glide中对并发处理还非常之多,一篇文章难以分析完,对于如何分析并发问题只能是大胆的假设,小心的推导求证。