1.在 CAS 中有三个参数:内存值 V、旧的预期值 A、要更新的值 B ,当且仅当内存值 V 的值等于旧的预期值 A 时,才会将内存值V的值修改为 B ,否则什么都不干。其伪代码如下:
if (this.value == A) {
this.value = B
return true;
} else {
return false;
}
2.J.U.C 下的 Atomic 类,都是通过 CAS 来实现的。下面就以 AtomicInteger 为例,来阐述 CAS 的实现。如下:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
Unsafe 是 CAS 的核心类,Java 无法直接访问底层操作系统,而是通过本地 native 方法来访问。不过尽管如此,JVM还是开了一个后门:Unsafe ,它提供了硬件级别的原子操作。
valueOffset 为变量值在内存中的偏移地址,Unsafe 就是通过偏移地址来得到数据的原值的。 value 当前值,使用 volatile 修饰,保证多线程环境下看见的是同一个。
1.我们就以 AtomicInteger 的 #addAndGet() 方法来做说明,先看源代码:
// AtomicInteger.java
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
// Unsafe.java
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
2.内部调用 unsafe 的 #getAndAddInt(Object var1, long var2, int var4)方法,在#getAndAddInt(Object var1, long var2, int var4) 方法中,主要是看 #compareAndSwapInt(Object var1, long var2, int var4, int var5) 方法,代码如下:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
该方法为本地方法,有四个参数,分别代表:对象、对象的地址、预期值、修改值
1.CAS 可以保证一次的读-改-写操作是原子操作,在单处理器上该操作容易实现,但是在多处理器上实现就有点儿复杂了。CPU 提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁。
1.CAS 虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:
用一个例子来阐述 ABA 问题所带来的影响。
有如下链表如下图:
假如我们想要把 A 替换为 B ,也就是 #compareAndSet(this, A, B) 。线程 1 执行 A 替换 B 操作之前,线程 2 先执行如下动作,A 、B 出栈,然后 C、A 入栈,最终该链表如下:
完成后,线程 1 发现仍然是 A ,那么 #compareAndSet(this, A, B) 成功,但是这时会存在一个问题就是 B.next = null,因此 #compareAndSet(this, A, B) 后,会导致 C 丢失,改栈仅有一个 B 元素,平白无故把 C 给丢失了。
1.CAS 的 ABA 隐患问题,解决方案则是版本号,Java 提供了 AtomicStampedReference 来解决。AtomicStampedReference 通过包装 [E,Integer] 的元组,来对对象标记版本戳 stamp ,从而避免 ABA 问题。对于上面的案例,应该线程 1 会失败。
2.AtomicStampedReference 的 #compareAndSet(...) 方法,代码如下:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
有四个方法参数,分别表示:预期引用、更新后的引用、预期标志、更新后的标志。源码部分很好理解:
Pair 为 AtomicStampedReference 的内部类,主要用于记录引用和版本戳信息(标识),定义如下:
// AtomicStampedReference.java
private static class Pair {
final T reference; // 对象引用
final int stamp; // 版本戳
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
private volatile Pair pair;
// AtomicStampedReference.java
public void set(V newReference, int newStamp) {
Pair current = pair;
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
}
下面,我们将通过一个例子,可以看到 AtomicStampedReference 和 AtomicInteger 的区别。我们定义两个线程,线程 1 负责将 100 —> 110 —> 100,线程 2 执行 100 —>120 ,看两者之间的区别。
public class Test {
private static AtomicInteger atomicInteger = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
public static void main(String[] args) throws InterruptedException {
// AtomicInteger
Thread at1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.compareAndSet(100,110);
atomicInteger.compareAndSet(110,100);
}
});
Thread at2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2); // at1,执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100,120));
}
});
at1.start();
at2.start();
at1.join();
at2.join();
// AtomicStampedReference
Thread tsf1 = new Thread(new Runnable() {
@Override
public void run() {
try {
//让 tsf2先获取stamp,导致预期时间戳不一致
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1
atomicStampedReference.compareAndSet(100,110,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
atomicStampedReference.compareAndSet(110,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
}
});
Thread tsf2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedReference.getStamp();
try {
TimeUnit.SECONDS.sleep(2); //线程tsf1执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference:" +atomicStampedReference.compareAndSet(100,120,stamp,stamp + 1));
}
});
tsf1.start();
tsf2.start();
}
}
运行结果:
运行结果充分展示了 AtomicInteger 导致的 ABA 问题,和使用 AtomicStampedReference 解决 ABA 问题。
如果你看到了这里,觉得文章写得不错就给个赞呗!欢迎大家评论讨论!如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足,定期免费分享技术干货。喜欢的小伙伴可以关注一下哦。谢谢!