推荐阅读
并发工具
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?
AtomicXxxx原子类
JUC包提供的AtomicXxxx原子类,分为五个类别:
1、原子化的基本数据类型
2、原子化的对象引用类型
3、原子化数组
4、原子化对象属性更新器
5、原子化的累加器
每个类别下,提供的工具类如下
原子化的基本数据类型:AtomicBoolean、AtomicInteger 和 AtomicLong。
原子化的对象引用类型:AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。
原子化数组:AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用这些原子类,我们可以原子化地更新数组里面的每一个元素。
原子化对象属性更新器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的。
原子化的累加器:LongAdder、DoubleAdder。LongAdder等累加器,是JDK 1.8提供的新工具类,是比AtomicLong还高效的无锁实现,从设计上就支持了更高的并发度,后续会持续更新。
21 | 并发工具:无锁原子类 详细介绍了各个类别的使用说明,可移步https://mp.weixin.qq.com/s/IzEacpL-wsTiiyKhTSRGfw 再回顾一下。
CAS无锁方案的实现原理
我们都知道,AtomicXxxx原子类的底层实现原理是CAS。
CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:
共享变量的内存值 A、
用于比较的值 B
共享变量的新值 C;
并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。
CAS用高级编程语言来实现,其实很简单:
(但在操作系统层面,用一条机器指令,实现这样的原子操作就不容易了)
public boolean compareAndSwap(int value, int expect, int update) {
//如果内存中的值value和期望值expect一样 则将值更新为新值update
if (value == expect) {
value = update;
return true;
} else {
return false;
}
}
代码只演示了CAS的主要逻辑。实际中 CAS是原子的,也就是“比较并交换”是是个原子、不可分割的。
如果要实现原子性,我们必须对“比较并交换” 这个代码块加锁。
试想,用Java实现CAS需要这么些行代码、才能实现,现在交给CPU一条指令去完成,难道不需要成本?
Compare And Swap,“比较并交换”,是CPU一条指令完成的,也就是CAS比较并交换天生就是原子的。
但是天下没有免费的午餐,CAS需要计算机硬件的支持;且我们使用CAS实现“比较并交换”,也有成本开销。
本质上,CAS是支持并发的元语,CAS在不同操作系统平台上都有不同的实现,因为CAS是和硬件相关的。
Java中如何使用CAS
Java是在Unsafe(sun.misc.Unsafe)类实现CAS的操作,而我们知道Java是无法直接访问操作系统底层的API的,而是通过 JNI(Java Native Interface)本地调用C/C++语言来实现CAS操作的。
各个操作系统平台,提供CAS的API后,Java核心类库只能通过JNI调用操作系统暴露的API,而Unsafe也屏蔽了不同平台的实现差异,在Java中可以一致的使用Unsafe完成CAS。
CAS这 些较底层的操作都是来自sun.misc.Unsafe类,这个类里面大多数方法都是native的,方法实现可以在openJdk的hotspot/share/vm/prims/unsafe.cpp里面找到。
注意,能够使用CAS 比较并交换的共享变量,必须用volatile修饰。
由volatile关键字修饰,可以保证可见性。才能保证多个线程在并发执行时,能够感知到值的变化。
以AtomicInteger为例,看看Unsafe底层是如何实现CAS的。
在AtomicInteger源码中,由内部的一个int域来保存值,且用volatile修饰:
//在AtomicInteger源码中,由内部的一个int域来保存值:
private volatile int value;
AtomicInteger#compareAndSet 调用的是 Unsafe#compareAndSwapInt,
Unsafe#compareAndSwapInt它的实现在unsafe.cpp里面:
/*
* Implementation of class sun.misc.Unsafe
*/
...
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
这里调用了Atomic的cmpxchg方法,继续找一下。这个方法定义在hotspot/share/vm/runtime/atomic.hpp中,实现在hotspot/share/vm/runtime/atomic.cpp中,最终实现取决于底层OS,比如linux x86,实现内联在hotspot部分代码os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp。
cmpxchg 其实就是操作系统OS级别提供的指令。
CAS并不是完美的
CAS 的一个不足点就是 ABA问题。
在多线程场景下CAS
会出现ABA
问题,关于ABA问题这里简单描述下,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这三个线程如下:
线程1,期望值为A,欲更新的值为B
线程2,期望值为A,欲更新的值为B
线程1
抢先获得CPU时间片,而线程2
因为其他原因阻塞了,线程1
取值与期望的A值比较,发现相等然后将值更新为B,然后这个时候出现了线程3
,期望值为B,欲更新的值为A,线程3取值与期望的值B比较,发现相等则将值更新为A,此时线程2
从阻塞中恢复,并且获得了CPU时间片,这时候线程2
取值与期望的值A比较,发现相等则将值更新为B,虽然线程2
也完成了操作,但是线程2
并不知道值已经经过了A->B->A
的变化过程。
可见,使用CAS更新value时,“相等”不代表“未被修改”。
如果不影响业务逻辑,则可以忽略。但某些场景下,ABA问题是致命的。
ABA导致修改丢失
如下一个case:
两个线程,操作一个栈,线程T1使用CAS替换栈顶,却造成线程T2的入栈、出栈操作丢失。
最终:线程2执行完修改后,线程1 通过CAS 将B 替换为head;造成C、D丢失。
ABA 解决之道
使用CAS 要提防ABA问题,如果不影响业务逻辑,则可以忽略。
解决ABA问题,思路很简单,就是加个version版本号,如果有人改动就自增1,Java也提供了工具类,就是原子化的对象引用类型:AtomicReference、AtomicStampedReference 和 AtomicMarkableReference。
详细说明可阅读 21 | 并发工具:无锁原子类
CAS的性能考量
唯一会制约CAS高效的因素是高并发,高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicLong等 原子化的包装类型 效率会降低。
如果并发较高,完全可以使用LongAdder替代AtomicLong 。
CAS与乐观锁
提到CAS,有人总会提乐观锁;
提到乐观锁。也总会再提CAS。二者究竟是什么关系呢?
先忘掉之前的模糊记忆吧!
乐观锁,是锁分类的一种;本质上,乐观锁的出现,是为提升悲观锁性能的。
实现乐观锁的一种思路是,加一个版本号,修改时通过版本号判定“是否有人修改”过。
而CAS具备“比较并交换”的能力,能够实现乐观锁。
总结
AtomicInteger#incrementAndGet 将 i++ 原本三条指令的工作,变成原子的、一步完成。在线程并发不高时,相比:
synchronized(this){
i++;
}
效率还是更高的。 无锁方案相对于互斥锁方案,优点非常多,而且不会存在因为加锁而产生的死锁问题。
但是前面提到,天下没有免费的午餐,AtomicXxxx无锁原子类借助硬件提供的CAS支持,实现了 原子执行 "i++" ;
CAS需要计算机硬件的支持;且我们使用CAS实现“比较并交换”,也有成本开销。这也是为什么会存在原子化对象属性更新器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater 的原因。
后续会专门分析 原子化对象属性更新器FieldUpdater,持续更新。
本文首发于 公众号 架构道与术(ToBeArchitecturer),欢迎关注、学习更多干货~
推荐阅读
并发工具
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?