文章目录
public class Test {
AtomicLong count = new AtomicLong(0);
void add10K() {
int idx = 0;
while (idx++ < 10000) {
count.getAndIncrement();
}
}
}
互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题.
1. 无锁方案的实现原理
其实原子类性能高的秘密很简单,硬件支持而已。CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。
模拟代码:只有当目前count的值和期望值expect相等时,才会将count更新为newValue。
class SimulatedCAS {
int count;
synchronized int cas(int expect, int newValue) {
// 读目前 count 的值
int curValue = count;
// 比较目前 count 值是否 == 期望值
if (curValue == expect) {
// 如果是, 则更新 count 的值
count = newValue;
} // 返回写入前的值
return curValue;
}
}
使用CAS来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。如果(2)返回值不等于count,表明count被其他线程改变。
class SimulatedCAS {
int count;
// 实现 count+=1
addOne(){
do{
newValue = count+1; //①
} while(count !=cas(count,newValue)) //②
synchronized int cas(int expect, int newValue) {
// 读目前 count 的值
int curValue = count;
// 比较目前 count 值是否 == 期望值
if (curValue == expect) {
// 如果是, 则更新 count 的值
count = newValue;
} // 返回写入前的值
return curValue;
}
}
// TODO
存在的ABA的问题:???
2. 看 Java 如何实现原子化的 count += 1
原子类 AtomicLong 的 getAndIncrement() 方法内部就是基于 CAS 实现
的。Java 1.8 版本中,getAndIncrement() 方法会转调unsafe.getAndAddLong() 方法。这里 this 和valueOffset 两个参数可以唯一确定共享变量的内存地址。
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
unsafe.getAndAddLong()方法的源码如下,该方法首先会在内存中读取共享变量的值,之后循环调用compareAndSwapLong()方法来尝试设置共享变量的值,直到成功为止。compareAndSwapLong()是一个native方法,只有当内存中共享变量的值等于expected时,才会将共享变量的值更新为x,并且返回true;否则返回fasle。compareAndSwapLong的语义和CAS指令的语义的差别仅仅是返回值不同而已。
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
// 读取内存中的值
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
// 原⼦性地将变量更新为 x
// 条件是内存中的值等于 expected
// 更新成功则返回 true
native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
CAS 使用的经典范例
do {
// 获取当前值
oldV = xxxx;
// 根据当前值计算新值
newV = ...oldV...
}while(!compareAndSet(oldV,newV);
3. 原子类概览
有五个类别:原子化的基本数据类型、原子化的对象引用类型、原子化数组、原子化对象属性更新器和原子化的累加器。
3.1 原子化的基本数据类型
AtomicBoolean、AtomicInteger和AtomicLong。
getAndIncrement() // 原⼦化 i++
getAndDecrement() // 原⼦化的 i--
incrementAndGet() // 原⼦化的 ++i
decrementAndGet() // 原⼦化的 --i
// 当前值 +=delta,返回 += 前的值
getAndAdd(delta)
// 当前值 +=delta,返回 += 后的值
addAndGet(delta)
//CAS 操作,返回是否成功
compareAndSet(expect, update)
// 以下四个⽅法
// 新值可以通过传⼊ func 函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)
3.2 原子化的对象引用类型
相关实现有 AtomicReference、AtomicStampedReference和AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。AtomicReference 提供的方法和原子化的基本数据类型差不多,这里不再赘述。不过需要注意的是,对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference和AtomicMarkableReference这两个原子类可以解决ABA问题。
解决ABA问题,增加版本号,每一次CAS,版本号都递增。AtomicStampedReference 实现的 CAS 方法就增加了版本号参数,
方法签名如下:
boolean compareAndSet(V expectedReference,V newReference,
int expectedStamp,int newStamp)
AtomicMarkableReference 的实现机制则更简单,将版本号简化成了一个 Boolean 值:
boolean compareAndSet(V expectedReference,V newReference,
boolean expectedMark,boolean newMark)
3.3 原子化数组
AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,原子化地更新数组里面的每一个元素。
3.4 原子化对象属性更新器
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和
AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的,创建更新器的方法如下:
public static <U> AtomicXXXFieldUpdater<U>
newUpdater(Class<U> tclass,String fieldName)
需要注意的是,对象属性必须是 volatile 类型的,只有这样才能保证可见性。
3.5 原子化的累加器
DoubleAccumulator、DoubleAdder、LongAccumulator和LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持compareAndSet()方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。