前言
在Android平台上,并发编程对于确保应用的响应性和用户体验至关重要。Kotlin协程库提供的lifecycleScope
和viewModelScope
是开发者常用的两个关键工具,它们分别通过CAS(Compare-And-Swap)和synchronized机制确保线程安全和协程的生命周期管理。本文将首先介绍CAS和synchronized两种并发控制机制,然后探讨它们在Android源码中的具体实现。
并发控制中的乐观锁与悲观锁
并发控制机制可以从不同角度进行分类,其中乐观锁和悲观锁是常见的两种方式。
乐观锁
乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为CAS(Compare-And-Swap)的技术来保证线程执行的安全性。由于乐观锁假设操作中没有锁的存在,因此不太可能出现死锁,换句话说,乐观锁天生免疫死锁。乐观锁多用于“读多写少”的环境,避免频繁加锁影响性能;而悲观锁多用于“写多读少”的环境,避免频繁失败和重试影响性能。
悲观锁
悲观锁总是假设每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。这种方式确保了数据的一致性,但也增加了性能开销,特别是在写操作频繁的场景中。synchronized关键字在Java中就是一种悲观锁的实现。
CAS (Compare-And-Swap) 机制
CAS(Compare-and-Swap)是一种被广泛应用在并发控制中的算法,它是一种乐观锁的实现方式,通过比较内存中的值与预期值,如果一致则更新为新值,否则重试。CAS操作的步骤如下:
- V:当前值。
- E:预期值(旧值)。
- N:新值。
CAS的工作原理
比较并交换的过程如下:
- 判断 V 是否等于 E。如果相等,将 V 的值设置为 N。
- 如果不相等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。
CAS操作的核心是保证这一过程的原子性,确保在多线程环境中只有一个线程能够成功更新变量,其余线程则重试或放弃。
示例
假设有一个共享变量 i
,初始值为 9。线程 A 尝试将其更新为 10:
- 线程 A 检查
i
是否为 9(E)。 - 如果
i
仍为 9,则将其更新为 10(N)。 - 如果
i
不是 9,说明其他线程已经修改过i
,此操作失败。
由于 CAS 是原子操作,底层通过CPU指令(如X86架构的cmpxchgl指令)保证其原子性。即使在多处理器环境中,也能通过lock
指令确保操作的完整性。
CAS在Java中的实现
在 Java 中,CAS 操作由Unsafe
类提供,该类包含一些本地方法,通过 JVM 使用 C 或 C++ 实现。
以下是Unsafe
类中一些关于CAS的方法:
boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset, int expected, int x);
boolean compareAndSwapLong(Object o, long offset, long expected, long x);
这些方法底层通过不同平台的特定指令实现原子操作。例如,Linux 的 X86 架构下,CAS 操作由cmpxchgl
指令完成。 Java 9 引入了 VarHandler类,这是Java 内存模型的一项重大改进,它提供了一种更灵活更高效的方式替代Unsafe。使得其更加安全和易用。
CAS 的优缺点
优点:
- 高性能:避免了锁的开销,适合高并发读操作的场景。
- 无阻塞:线程无需等待锁释放,可以直接重试,提高了系统的吞吐量。
缺点:
- ABA问题:值可能被其他线程修改再恢复原值,导致错误判断。可以通过版本号或时间戳解决。
- 复杂性:实现复杂,难以调试和维护。
synchronized机制
Synchronized是Java提供的关键字,用于在多个线程之间实现同步。它通过内置锁保证在同一时间只有一个线程可以访问被同步的代码块,确保线程安全。
synchronized的优缺点
优点:
- 简单易用:语法简单,易于理解和使用。
- 严格顺序:保证操作的严格顺序,适合需要顺序执行的场景。
缺点:
- 性能开销:频繁的锁竞争会带来较大的性能开销,可能导致线程阻塞。
- 死锁风险:不当使用可能会导致死锁,影响系统稳定性。
LifecycleScope的CAS实现
在Android的lifecycleScope
实现中,利用了CAS机制来确保线程安全。以下是Lifecycle.coroutineScope
属性的实现代码:
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
这段代码通过不断尝试更新原子引用,确保只创建一个LifecycleCoroutineScope
实例,体现了CAS机制的乐观锁策略。
ViewModelScope的synchronized实现
相对于Lifecycle.coroutineScope
的CAS实现,ViewModel.viewModelScope
采用了Synchronized机制来保证线程安全。以下是ViewModel.viewModelScope
属性及其辅助函数setTagIfAbsent
的实现代码:
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
private fun <T> setTagIfAbsent(key: String, newValue: T): T {
synchronized(mBagOfTags) {
val previous = mBagOfTags.get(key) as T?
if (previous == null) {
mBagOfTags.put(key, newValue)
}
return previous ?: newValue
}
}
这段代码使用了synchronized关键字来保护对mBagOfTags
映射的访问,确保在多个线程尝试更新同一个ViewModel的viewModelScope
时,只有一个能够成功。
并发控制优化策略
在实际应用中,正确选择并发控制策略对于性能和稳定性至关重要。CAS提供了无锁的解决方案,适用于并发写操作较少的场景;而synchronized则适用于写操作较频繁或需要保证操作顺序的场景。开发者在实现特定功能时需要考虑如何平衡这两种策略的利弊,以实现最佳性能和可靠性。
结论
lifecycleScope
和viewModelScope
在Android并发编程中提供了有效的线程同步机制。通过深入理解它们的实现,开发者可以更好地管理协程的生命周期,编写既安全又高效的并发代码。
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。