对原子类知识进行梳理。
目录
二、AtomicBoolean和AtomicReference
三、AtomicStapedReference和AtomicMarkableReference
2.为什么没有AtomicStampedInteger或AtomicStampedLong
四、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater
五、AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray
六、LongAdder、LongAccumulator、DoubleAdder和DoubleAccumulator
5. DoubleAdder和DoubleAccumulator
前言
对原子类知识进行梳理。原子类基于CAS函数实现,采用的乐观锁的实现方式。
Unsafe提供了Integer、Long、Object的CAS操作函数。
对应的类有AtomicInteger、AtomicLong、AtomicReference
对ABA问题的解决:AtomicStapedReference、AtomicMarkableReference。通过引入版本号来实现,要同时进行两个值的CAS所以只能实现引用对象的Atomic类。Staped是Long类型的版本号,Markable是Boolean类型的版本号。
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater,对已经存在的类的成员变量进行原子操作。
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray提供数组元素的原子操作。
LongAdder最终一致性,用于高并发且不要求强一致的情况下,采用拆分的思想。
LongAccumulator可以定义二元操作符,且可以传入一个初始值。
一、AtomicInteger和AtomicLong
对于一个整数的加减操作,要保证线程安全,需要加锁,也就是加synchronized锁。但有了Concurrent包的Atomic相关的类之后,synchronized关键字可以用AtomicInteger代替,其性能更高好。
其原理就是使用了Unsafe的CAS函数。
CAS是一种乐观锁,作者认为数据发生并发冲突的概率很小,所以读操作不上锁。等到写操作的时候,再判断数据在此期间是否被其他线程修改了。如果被其他线程修改了,就把新数据重新读出来,重复这个过程。
悲观锁:作者认为数据发生并发冲突的概率很大,所以读操作之前就上锁。
Unsafe的CAS详解:
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
第一个是对象,第二个是对象的成员变量,第三个是读出来的旧值,第四个是新的值。
第二个参数是一个long类型的整数,意思是某个成员变量在对应的类中的内存偏移量。
可以通过objectFieldOffset(Field var1);获取
执行CAS操作的时候不是直接操作value,而是操作valueOffset。
二、AtomicBoolean和AtomicReference
1.为什么需要AtomicBoolean
因为存在下面这种情况。
if (flag == true) flag = false;
2.如何支持boolean和double类型
在unsafe类中,只提供了三种类型的CAS:int、long、Object
boolean:将boolean转化为int再转化回boolean。
double:将double转换为long类型再转换为double。Double.doubleToRawLongBits(Double.longBitsToDouble(b) + x)
三、AtomicStapedReference和AtomicMarkableReference
1.ABA问题与解决方法
到目前为止CAS都是基于值做比较的。如果另一个线程将值修改为B再修改回A,那么对于当前线程是无法感知到,当前线程认为该值未被修改过。这就是ABA问题。
在解决ABA问题,不仅要比较值还要比较一个版本号,这就是AtomicStapedReference所做的事情。
2.为什么没有AtomicStampedInteger或AtomicStampedLong
Integer和Long类需要同时比较两个值,进行CAS的时候需要将Integer或Long和版本号一起形成一个Pair对象,然后再进行CAS操作进行比较。
3.AtomicMarkableReference
采用的版本号是boolean类型的,无法解决ABA问题,但可以降低ABA出现的概率。
四、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater
1.为什么需要AtomicXXXFieldUpdater
如果一个类是自己写的,可以在编写的时候把成员函数定义为Atomic类型。
但是如果是一个已经存在的类,要想实现对其成员变量的原子操作,就需要AtomicXXXFieldUpdater。
五、AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray
这里并不是说对整个数组的操作是原子的,而是针对数组中一个元素的原子操作而言。
偏移量的计算使用i * scale + base。
六、LongAdder、LongAccumulator、DoubleAdder和DoubleAccumulator
1.LongAdder原理
AtomicLong内部是一个volatile long型变量,由多个线程对这个变量进行CAS操作。多个线程同时对一个变量进行CAS操作,在高并发的场景下仍不够快,如果要再提高性能,把一个变量拆成多份,变成多个变量,有些类似于ConcurrentHashMap的分段锁的例子。
如图所示,把一个变量拆成了一个base变量和多个Cell,每个Cell包装了一个Long型变量。
当多个线程并发累加的时候
(1)并发程度低的话,直接加到base变量上;
(2)并发程度高的话,平摊到Cell上。
最后取值的时候再把base和这些Cell求sum运算。
public long sum() {
//通过累加base与cells数组中的value从而获得sum
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
最sum求和函数中,并没有对cells[]数组加锁。在执行求和操作时可能存在其他线程修改数组里的值。它是最终一致性,而不是强一致性。
适用于高并发且不要求严格同步的场景。
2.伪共享和缓存行填充
@sun.misc.Contended用于解决伪共享和缓存行填充。
abstract class Striped64 extends Number {
static final int NCPU = Runtime.getRuntime().availableProcessors();
transient volatile Cell[] cells;
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
}
缓存与主内存进行数据交互的基本单位是Cache Line缓存行。当读入了X、Y、Z三个变量时,他们可能在同一个缓存行中,修改了X变量,会导致整个缓存行失效,Y、Z变量也失效了,这就是伪共享问题。
问题的原因在于X、Y、Z变量在同一个缓存行中,要解决这个问题需要用到缓存行填充,分别在X、Y、Z变成后面加上7个无用的Long型,填充整个缓存行,让X、Y、Z处于三行不同的缓存行。
3.LongAdder核心实现
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
/**
* 如果是第一次执行,则直接case操作base
*/
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
/**
* as数组为空(null或者size为0)
* 或者当前线程取模as数组大小为空
* 或者cas更新Cell失败
*/
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
第一次尝试加在base上,第二次尝试加在Cell上。如果都失败,调用longAccumulate函数。
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
/**
* 若getProbe为0,说明需要初始化
*/
ThreadLocalRandom.current();
h = getProbe();
wasUncontended = true;
}
//写入冲突的标志
boolean collide = false;
/**
* 失败重试
*/
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
/**
* 若as数组已经初始化,(n-1) & h 即为取模操作,相对 % 效率要更高
*/
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) {
Cell r = new Cell(x);
if (cellsBusy == 0 && casCellsBusy()) {//这里casCellsBusy的作用其实就是一个spin lock
//可能会有多个线程执行了`Cell r = new Cell(x);`,
//因此这里进行cas操作,避免线程安全的问题,同时前面在判断一次
//避免正在初始化的时其他线程再进行额外的cas操作
boolean created = false;
try {
Cell[] rs; int m, j;
//重新检查一下是否已经创建成功了
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot 现在是非空了,continue到下次循环重试
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;//若cas更新成功则跳出循环,否则继续重试
else if (n >= NCPU || cells != as) // 最大只能扩容到CPU数目, 或者是已经扩容成功,这里只有的本地引用as已经过期了
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // 扩容
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//重新计算hash(异或)从而尝试找到下一个空的slot
h = advanceProbe(h);
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
/**
* 默认size为2
*/
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x : // 若已经有另一个线程在初始化,那么尝试直接更新base
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
初始化时容量为2,每次扩容扩大为2倍,不能超过CPU数量。
4.LongAccumulator
和LongAdder的区别就在于下面一句话
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
LongAccumulator支持自定义一个二元操作符,并且可以传入一个初始值。
5. DoubleAdder和DoubleAccumulator
DoubleAdder其实也是用Long型实现的,因为没有double类型的CAS函数。
Double.doubleToRawLongBits(Double.longBitsToDouble(b) + x)