参考
- Java CAS ABA问题发生的场景分析 提到了ABA问题
- Unsafe$compareAndSwapInt 提到AtomicInteger.compareAndSwapInt调用了Unsafe.compareAndSwapInt。后者是用valueOffset(变量偏移量)来定位变量的。
- cas和aba问题 讲了AtomicStampedReference的实现
- valueOffset的作用和unsafe.compareAndSwapObject的比较原理 讲了valueoffset只是相对于"this"对象的相对偏移量,unsafe.compareAndSwapObject只是比较内存地址是否相等。
1. AtomicInteger实现CAS
使用
AtomicInteger
、
AtomicBoolean
等原子操作类可以完成原子操作。它的各种操作都是基于
Unsafe
类的,你可以看到函数的画风都是下面这样:
// AtomicIneger.java
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
复制
但
Unsafe
的实现是native方法,无法查看源码:
// Unsafe.java
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
复制
2. 底层原子操作:Unsafe类
参考Unsafe类
Unsafe涉及底层硬件级别的原子操作,其函数大多是native的,如下:
// Unsafe.java
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
复制
程序员不能直接调用它,只有授信的代码才能获得该类的实例,当然JDK库里面的类是可以随意使用的。
我们只需要知道,它是原子操作的底层实现即可。
3. volatile + AtomicIntegerFieldUpdater实现CAS:netty中的应用
参考张龙netty教程的P.81~P.84
先说结论:volatile + AtomicIntegerFieldUpdater的实现方案相比AtomicInteger能节省更多内存空间。
3.1 两种int的CAS实现
-
类实现了CAS,但如果频繁使用其实例,会占用不少内存:64位机器上,一个AtomicInteger
对象地址占用8个字节,其内部维护的int值AtomicInteger
占用4个字节。也就是说,在每个用到CAS操作的整形值的地方,都需要维护一个value
对象,也就是占用4+8=12字节。AtomicInteger
- 但如果能用
+volatile
的方法实现,就能减少存储。你只需要维护一个静态变量AtomicIntegerFieldUpdater
,并为需要用到CAS操作的int值声明为AtomicIntegerFieldUpdater
即可。只需要先分配一个静态变量,然后在每个用到CAS操作的整形值的地方,都只需要维护一个volatile
变量,也就是占用4字节。volatile int
3.2 netty的CAS实现方案的选择
在netty中,需要频繁创建/销毁大量的ByteBuf对象,用于存储信息。为了及时销毁ByteBuf对象,需要使用引用计数。netty构建了
AbstractReferenceCountedByteBuf
,实现了引用计数的功能。
在程序运行的过程中,会创建大量的
AbstractReferenceCountedByteBuf
实例,每个需要在其内部维护一个整形值,代表引用计数。实例可能会被不同对象同时引用,当它不被引用时,应当及时释放空间。为了让引用计数在多线程下有序地增减,引用计数应当用CAS实现。
那么我们要使用
AtomicInteger
实现CAS吗?
-
实例里的引用计数用一个AbstractReferenceCountedByteBuf
变量实现,确实简单。但这样每个实例都要为它分配4+8=12字节的空间。AtomicInteger
- 但如果我们用
+volatile int
,只需先为静态变量AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater
分配空间,然后每个实例只需要占用4字节即可。能节约很多内存。
正因为如此,netty使用了
来实现引用计数的CAS。AtomicIntegerFieldUpdater
3.3 从netty源码学习CAS的实现
我们看下源码:
// AbstractReferenceCountedByteBuf.java
public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
private static final long REFCNT_FIELD_OFFSET;
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
private volatile int refCnt = 1;
...
@Override
public ByteBuf retain(int increment) {
return retain0(checkPositive(increment, "increment"));
}
private ByteBuf retain0(final int increment) {
int oldRef = refCntUpdater.getAndAdd(this, increment);
...
return this;
}
...
}
复制
AbstractReferenceCountedByteBuf
通过retain和release来增加、释放引用。而这两个函数是基于
refCntUpdater
对
refCnt
的CAS操作。
这样,每个
AbstractReferenceCountedByteBuf
实例只需要为
refCnt
分配4字节空间即可,相比于
AtomicInteger
的12字节,节省了很多空间。
4. ABA问题
参考什么是CAS问题
AtomicInteger、AtomicReference等类会带来ABA问题。
ABA问题,就是要维护的变量被替换后,又设置回来。类实例将无法辨别它被替换过。
举个例子,假设有一个变量x:
- 线程1试图用cas把x从A设置为C,所以它先查询x的值。(在这瞬间,线程切换)
- 线程2用cas把x设置为B
- 线程2用cas把x设置为A
- (线程切换回来)线程1查询到x的值为A,于是cas理所当然地把x改为了C。
问题是:线程1在查询x的过程中,x的值已经经历了A->B->A的转变,而线程1对此无所知。这就是ABA问题了。
4.1 AtomicReference的ABA问题
我们看到compareAndSet的源码:
// AtomicReference.java
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
复制
根据注释,它的比较是基于地址数值比较的,而与"equals"方法没有关系。stackoverflow上的问题 也印证了这一点。
也就是说,AtomicReference的ABA问题指的是"其引用可能会改变",而不是指"引用的变量的值可能会改变"。
解决ABA问题: AtomicStampedReference<T>
AtomicStampedReference