简介
在多线程或高并发的环境中,对数值进行++或–操作时就会导致预期的值与实际的值不相同,我们可以通过使用synchroized加锁的操作来对数值进行++或–的操作,这样就可以保证预期的值与实际的值相同,但是使用synchroized就会使性能下降,此时就可以使用Atomic原子类对数值进行操作。
AtomicInteger
//获取unsafe实例对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
//value所在的偏移量
private static final long valueOffset;
static {
try {
//获取value所在的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//以volatile修饰这样在多线程运行下该值被修改能保证其它线程立即感知到
//volatile能保证可见性 有序性(防止指令重排)
//volatile底层是通过内存屏障来保证有序性
//volatile在多线程下如果该值被其它线程修改了则立马会将修改的值刷入到主内存中
//并将其它线程的工作内存中的局部变量过期,并立即从主内存中重新获取值到工作内存中
private volatile int value;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
我们主要是看AtomicInteger中的incrementAndGet()方法和decrementAndGet()方法,从上面的代码中来看incrementAndGet()方法和decrementAndGet()方法都是调用的unsafe中的getAndAddInt()方法。this代表当前AtomicInteger对象,valueOffset则是当前value值所在的偏移量,1和-1则代表加1或减1操作,而后面的+1和-1则是调用getAndAddInt()方法之后返回的value不是操作之后的value,而是操作之前的value,则需要对操作之前的value进行+1或-1并返回给用户。
getAndAddInt()
/**
* var1 当前AtomicInteger对象
* var2 value所在的偏移量
* var4 加1或减1
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//根据偏移量获取AtomicInteger中的value
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//返回获取到的value值,而不是操作之后的value值
return var5;
}
getAndAddInt()方法先根据value所在的偏移量var2获取AtomicInteger中最新的value值var5,然后使用获取到的最新的value值var5与AtomicInteger中的偏移量为var2的value值进行比较,如果相同则与var4进行相加,如果不相同则继续上面的步骤。为什么先要获取到value值再进行比较,而不是直接修改,因为在刚获取到value值的时候可能别的线程已经对value值进行了操作,所以需要对value进行比较,compareAndSwapInt()方法具有原子性,所以不需要担心在比较和修改value的时候其它线程对value进行修改。
AtomicLong与AtomicInteger的问题
AtomicLong、AtomicBoolen与AtomicInteger调用的方法相同,都是通过CAS操作来修改值,AtomicLong与AtomicInteger都会出现ABA问题与自旋问题。ABA问题则是当线程1根据偏移量获取到value为1,此时线程2对value进行加1操作,value变成了2,线程3则对value减1操作,value又变成了1,此时线程1拿着根据偏移量获取到的value进行比较并修改,虽然说线程1修改成功,但是线程1在准备执行比较并修改操作之前,value发生了改变。自旋问题则是getAndAddInt()方法中使用的是do while语句,如果在高并发的情况下,var5的值一直被修改就会导致while中的条件一直不成立则会一直执行do while。
LongAdder
/**
* 自增1
*/
public void increment() {
add(1L);
}
/**
* 递减1
*/
public void decrement() {
add(-1L);
}
/**
* 添加指定的值
*/
public void add(long x) {
//在多线程的情况下将每个线程分开操作而不是都对base进行操作
//并将操作后的数值放入Cell数组中
//在没有线程竞争的情况下只会对base进行操作
Cell[] as;
//b 基础值
//v cell对象中的value
long b, v;
int m;
Cell a;
//cells不为空说明已经出现过线程竞争
//cells为空说明还未出现过线程竞争,则对base进行CAS操作
if ((as = cells) != null || !casBase(b = base, b + x)) {
//出现了线程竞争或者对base执行CAS操作失败则会进入到当前if语句中
//是否未出现竞争
boolean uncontended = true;
//cells数组不为空但是根据Probe值进行运算获取到数组中的指定索引位置上的cell对象为空
//则说明指定索引位置上的cell对象还未进行初始化,则执行longAccumulate方法对cell对象进行初始化
//cells数组不为空并且根据Probe值进行运算获取到数组中的指定索引位置上的cell对象也不为空
//则会尝试着对cell对象中的value执行cas操作
//如果执行失败则说明出现了竞争,其它线程可能正在对该索引位置上的cell对象执行操作
//此时就会执行longAccumulate方法进行自旋重新对该对象进行操作
if (as == null || (m = as.length - 1) < 0
//Cell数组不为空则使用Probe值与m进行与运算获取到数组中的Cell对象
|| (a = as[getProbe() & m]) == null
//获取到的Cell对象不为空则执行CAS操作将指定的值更新到获取到的Cell对象中的value
|| !(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
public long 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;
}
DoubleAdder与LongAdder大致相同,我们以LongAdder为例,主要是看increment()、decrement()、sum()这三个方法,increment(),与decrement()方法都是调用的add()方法,在多线程的情况下则是将每个线程分开操作cell对象,而不是对base进行操作,在没有线程竞争的情况下则对base进行操作,sum()方法则是将cells数组中的所有cell对象的value与base相加。
-
如果cells数组不为空则使用Probe值进行运算获取到数组指定索引位置上的cell对象,如果cell对象为空则调用longAccumulate()方法创建指定索引位置上的cell对象。
-
如果cell对象不为空则尝试对该对象中的value进行CAS操作,如果执行失败则说明该对象正在被其它线程更新,则会调用longAccumulate()方法进行自旋尝试更新。
-
如果cells数组为空则说明未出现过线程竞争,则会尝试对base进行CAS操作,如果失败则会调用longAccumulate()方法对cells数组进行初始化。
longAccumulate
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
//存储线程的Probe值
int h;
//如果线程的Probe值等于0则强制初始化
if ((h = getProbe()) == 0) {
//强制初始化
ThreadLocalRandom.current();
//重新获取Probe值
h = getProbe();
//线程的Probe值还未初始化说明还没有发生线程竞争
wasUncontended = true;
}
//是否发生竞争
boolean collide = false;
for (; ; ) {
Cell[] as;
Cell a;
//as数组长度
int n;
long v;
//校验cells是否不为空
//如果cells不为空则说明已经初始化过
if ((as = cells) != null && (n = as.length) > 0) {
//根据线程的Probe值与cells数组的长度-1进行与运算获取到指定索引位置上的cell对象
//如果cell对象为null则说明cell对象还没有被其它线程初始化
if ((a = as[(n - 1) & h]) == null) {
//当前线程的cell对象为空
//校验当前cell对象的锁是否被其它线程持有
if (cellsBusy == 0) {
//未被其它线程持有
//创建cell对象并将x值赋值给cell对象中的value
Cell r = new Cell(x);
//再次校验当前cell对象的锁是否被其它线程持有并尝试获取锁
if (cellsBusy == 0 && casCellsBusy()) {
//是否创建成功
boolean created = false;
try {
Cell[] rs;
int m, j;
//如果cells数组不为空则根据Probe值重新获取cells数组中指定索引位置上的cell对象
//如果数组中指定索引位置上的cell对象为空则将创建的cell对象赋与指定索引位置
//如果数组中指定索引位置上的cell对象不为空则有可能发生了扩容
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;
}
}
collide = false;
//当前线程所在的cell不为空并且cas更新失败
//更新失败的原因可能是因为有其它线程正在对该cell对象进行更新
//此时当前线程需要将wasUncontended设置为true说明有其它线程正在竞争
//当前线程自旋一次等待下次的更新
} else if (!wasUncontended)
wasUncontended = true;
//当线程更新失败的时候则会自旋一次
//自旋之后则会执行当前判断语句中的CAS操作
//当前CAS操作执行成功则退出循环
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
//如果当前cells数组的长度到达了cpu的核心数或者cells已经发生了扩容
//则将collide设置为false并执行advanceProbe重新获取线程的Probe值
else if (n >= NCPU || cells != as)
collide = false;
//如果上上个if语句中执行CAS操作失败了或者上个if语句条件不成立则说明出现了线程竞争
else if (!collide)
//将collide设置为true说明出现了竞争的情况
collide = true;
//出现了竞争的情况尝试获取锁并扩容
else if (cellsBusy == 0 && casCellsBusy()) {
try {
//获取锁成功
//校验cells是已经被其它线程扩容
if (cells == as) {
//未被其它线程扩容则当前线程执行扩容
//扩容大小与原数组的两倍
Cell[] rs = new Cell[n << 1];
//循环将旧数组元素拷贝到新的数组中
for (int i = 0; i < n; ++i)
rs[i] = as[i];
//重新赋值cells为新数组
cells = rs;
}
} finally {
//释放锁
cellsBusy = 0;
}
//已竞争问题
collide = false;
//使用扩容后的新数组重试执行操作
continue;
}
//重新获取线程的Probe值
h = advanceProbe(h);
//其它线程还未加锁并且cells未被初始化
//则调用casCellsBusy()方法进行CAS操作将cellsBusy设置为1
//当其它线程看到cellsBusy的值设置为1时则说明已经有线程在初始化
} else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
//是否初始化成功
boolean init = false;
try {
//校验as是否与cells相同
//如果不相同则说明在获取锁的时候其它线程可能已经对cells数组进行了初始化
if (cells == as) {
//创建大小为2的cell数组
Cell[] rs = new Cell[2];
//使用线程的Probe值进行与运算获取到cell数组中的索引位置
//并创建指定索引位置上的cell对象
//在创建cell对象的时候已经将x的值赋值给了cell对象中的value
//相当于已经执行了数值操作
rs[h & 1] = new Cell(x);
//将创建的数组赋值给cells
cells = rs;
//初始化成功
init = true;
}
} finally {
//释放锁
cellsBusy = 0;
}
//校验是否初始化成功
//成功则退出当前循环
if (init)
break;
//当其它线程正在初始化cells数组
//当前线程则对base执行CAS操作
//如果CAS操作失败则校验cells是否初始化完成
//如果未完成则校验其它线程是否在初始化cells数组
//如果其它线程正在初始化则继续对base执行CAS操作
} else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
}
}
总结:longAdder和DoubleAdder并没有解决ABA问题与自旋的问题,只不过是优化了自旋的问题,longAdder和DoubleAdder中在多线程下不是直接对base进行操作的,而是将每个线程分开对cells数组中的对象进行操作的,只有在没有线程竞争的情况下才会对base进行操作,如果一个线程长时间对cell对象操作失败,则会选择其它cell对象进行操作,而不是一直对一个cell对象操作,这样就可以避免长时间自旋的问题。
上面所讲的Atomic都只能对数值进行操作,如果想对一个对象进行操作那就需要使用AtomicReference,既能对数值操作也能对对象进行操作,AtomicReference解决了自旋的问题,在AtomicReference中去掉了循环操作,直接对value进行CAS操作,不管执行是否成功都直接返回执行结果不再重试,用户在编写代码的时候可以根据执行的结果来决定是否需要重试,如果执行失败的时候调用循环进行重试的话这样还是会有自旋的问题。
AtomicStampedReference
private static class Pair<T> {
//引用对象
final T reference;
//邮戳
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
//根据引用对象和邮戳创建Pair对象
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
/**
* 存放引用对象和邮戳的对象实例
*/
private volatile Pair<V> pair;
/**
* 创建一个指定的引用对象并且附带指定的邮戳
* @param initialRef 引用对象
* @param initialStamp 邮戳
*/
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
/**
* 获取引用的对象
* @return
*/
public V getReference() {
return pair.reference;
}
/**
* 获取邮戳
* @return
*/
public int getStamp() {
return pair.stamp;
}
/**
* 通过cas操作将预期的引用对象和邮戳修改成新的
* @param expectedReference 预期的引用对象,通过getReference方法获取
* @param newReference 新的引用对象
* @param expectedStamp 预期的邮戳,通过getStamp方法获取
* @param newStamp 新的邮戳
* @return
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
//获取当前线程中的pair对象
Pair<V> current = pair;
return
//当前线程中的引用对象是否与预期的引用对象相同
expectedReference == current.reference &&
//当前线程中的邮戳是否与预期的邮戳相同
expectedStamp == current.stamp &&
//如果当前线程中的引用对象和邮戳与新的引用对象和邮戳相同则不执行cas操作
((newReference == current.reference &&
newStamp == current.stamp) ||
//当前线程中的引用对象和邮戳与预期和新的引用对象和邮戳不相同
//执行cas操作将新的引用对象和邮戳赋值给当前线程中的pair对象
casPair(current, Pair.of(newReference, newStamp)));
}
AtomicStampedReference解决了ABA问题以及自旋的问题,自旋的问题与AtomicReference一样去掉了循环的操作,ABA问题则是通过一个邮戳来解决,也可以理解成版本号,当线程1获取到预期的引用对象和版本号,此时版本号为1,在线程1执行获取操作之后还未执行CAS操作时,线程2修改了引用对象并将版本号设置成了2,线程3则将引用对象修改成了与线程1获取到的引用对象相同,并将版本号设置成了3,此时线程1执行修改的时候虽然引用对象相同但是最新的版本号与预期的版本号不相同,则不会执行CAS操作。