文章目录
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)) //②
// 模拟实现 CAS,仅⽤来帮助理解
synchronized int cas(int expect, int newValue) {
// 读目前 count 的值
int curValue = count;
// 比较目前 count 值是否 == 期望值
if (curValue == expect) {
// 如果是, 则更新 count 的值
count = newValue;
} // 返回写入前的值
return curValue;
}
}
存在的ABA的问题:
前面我们提到“如果 cas(count,newValue) 返回的值不等于count,意味着线程在执行完代码①处之后,执行代码②处之前,count的值被其他线程更新过”,那如果cas(count,newValue) 返回的值等于count,是否就能够认为count的值没有被其他线程更新过呢?显然不是的,假设count原本是A,线程 T1 在执行完代码①处之后,执行代码②处之前,有可能 count 被线程 T2 更新成了 B,之后又被 T3 更新回了 A,这样线程 T1 虽然看到的一直是 A,但是其实已经被其他线程更新过了,这就是 ABA 问题。
可能大多数情况下我们并不关心 ABA 问题,例如数值的原子递增,但也不能所有情况下都不关
心,例如原子化的更新对象很可能就需要关心 ABA 问题,因为两个 A 虽然相等,但是第二个 A
的属性可能已经发生变化了。所以在使用 CAS 方案的时候,一定要先check 一下。
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()方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。
4.总结
无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试)。
Java提供的原子类能够解决一些简单的原子性问题,但你可能会发现,上面我们所有原子类的方
法都是针对一个共享变量的,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方
案。原子类虽好,但使用要慎之又慎。
5.课后思考
下面的示例代码是合理库存的原子化实现,仅实现了设置库存上限 setUpper() 方法,你觉得
setUpper() 方法的实现是否正确呢?
public class SafeWM {
class WMRange {
final int upper;
final int lower;
WMRange(int upper, int lower) {
// 省略构造函数实现
}
}
final AtomicReference<WMRange>
rf = new AtomicReference<>(new WMRange(0, 0));
// 设置库存上限
void setUpper(int v) {
WMRange nr;
WMRange or = rf.get();
do {
// 检查参数合法性
if (v < or.lower) {
throw new IllegalArgumentException();
}
nr = new WMRange(v, or.lower);
} while (!rf.compareAndSet(or, nr));
}
}