死磕 java并发包之AtomicStampedReference源码分析(ABA问题详解)
问题
(1)什么是ABA?
(2)ABA的危害?
(3)ABA的解决方法?
(4)AtomicStampedReference是什么?
(5)AtomicStampedReference是怎么解决ABA的?
简介
AtomicStampedReference是java并发包下提供的一个原子类,它能解决其它原子类无法解决的ABA问题。
ABA
ABA问题发生在多线程环境中,当某线程连续读取同一块内存地址两次,两次得到的值一样,它简单地认为“此内存地址的值并没有被修改过”,然而,同时可能存在另一个线程在这两次读取之间把这个内存地址的值从A修改成了B又修改回了A,这时还简单地认为“没有修改过”显然是错误的。
比如,两个线程按下面的顺序执行:
(1)线程1读取内存位置X的值为A;
(2)线程1阻塞了;
(3)线程2读取内存位置X的值为A;
(4)线程2修改内存位置X的值为B;
(5)线程2修改又内存位置X的值为A;
(6)线程1恢复,继续执行,比较发现还是A把内存位置X的值设置为C;
可以看到,针对线程1来说,第一次的A和第二次的A实际上并不是同一个A。
ABA问题通常发生在无锁结构中,用代码来表示上面的过程大概就是这样:
public class ABATest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
new Thread(()->{
int value = atomicInteger.get();
System.out.println("thread 1 read value: " + value);
// 阻塞1s
LockSupport.parkNanos(1000000000L);
if (atomicInteger.compareAndSet(value, 3)) {
System.out.println("thread 1 update from " + value + " to 3");
} else {
System.out.println("thread 1 update fail!");
}
}).start();
new Thread(()->{
int value = atomicInteger.get();
System.out.println("thread 2 read value: " + value);
if (atomicInteger.compareAndSet(value, 2)) {
System.out.println("thread 2 update from " + value + " to 2");
// do sth
value = atomicInteger.get();
System.out.println("thread 2 read value: " + value);
if (atomicInteger.compareAndSet(value, 1)) {
System.out.println("thread 2 update from " + value + " to 1");
}
}
}).start();
}
}
打印结果为:
thread 1 read value: 1
thread 2 read value: 1
thread 2 update from 1 to 2
thread 2 read value: 2
thread 2 update from 2 to 1
thread 1 update from 1 to 3
ABA的危害
为了更好地理解ABA的危害,我们还是来看一个现实点的例子。
假设我们有一个无锁的栈结构,如下:
public class ABATest {
static class Stack {
// 将top放在原子类中
private AtomicReference<Node> top = new AtomicReference<>();
// 栈中节点信息
static class Node {
int value;
Node next;
public Node(int value) {
this.value = value;
}
}
// 出栈操作
public Node pop() {
for (;;) {
// 获取栈顶节点
Node t = top.get();
if (t == null) {
return null;
}
// 栈顶下一个节点
Node next = t.next;
// CAS更新top指向其next节点
if (top.compareAndSet(t, next)) {
// 把栈顶元素弹出,应该把next清空防止外面直接操作栈
t.next = null;
return t;
}
}
}
// 入栈操作
public void push(Node node) {
for (;;) {
// 获取栈顶节点
Node next = top.get();
// 设置栈顶节点为新节点的next节点
node.next = next;
// CAS更新top指向新节点
if (top.compareAndSet(next, node)) {
return;
}
}
}
}
}
咋一看,这段程序似乎没有什么问题,然而试想以下情形。
假如,我们初始化栈结构为 top->1->2->3,然后有两个线程分别做如下操作:
(1)线程1执行pop()出栈操作,但是执行到if (top.compareAndSet(t, next)) {
这行之前暂停了,所以此时节点1并未出栈;
(2)线程2执行pop()出栈操作弹出节点1,此时栈变为 top->2->3;
(3)线程2执行pop()出栈操作弹出节点2,此时栈变为 top->3;
(4)线程2执行push()入栈操作添加节点1,此时栈变为 top->1->3;
(5)线程1恢复执行,比较节点1的引用并没有改变,执行CAS成功,此时栈变为 top->2;
What?点解变成 top->2 了?不是应该变成 top->3 吗?
那是因为线程1在第一步保存的next是节点2,所以它执行CAS成功后top节点就指向了节点2了。
测试代码如下:
private static void testStack() {
// 初始化栈为 top->1->2->3
Stack stack = new Stack();
stack.push(new Stack.Node(3));
stack.push(new Stack.Node(2));
stack.push(new Stack.Node(1));
new Thread(()->{
// 线程1出栈一个元素
stack.pop();
}).start();
new Thread(()->{
// 线程2出栈两个元素
Stack.Node A = stack.pop();
Stack.Node B = stack.pop();
// 线程2又把A入栈了
stack.push(A);
}).start();
}
public static void main(String[] args) {
testStack();
}
在Stack的pop()方法的if (top.compareAndSet(t, next)) {
处打个断点,线程1运行到这里时阻塞它的执行,让线程2执行完,再执行线程1这句,这句执行完可以看到栈的top对象中只有2这个节点了。
记得打断点的时候一定要打Thread断点,在IDEA中是右击选择Suspend为Thread。
通过这个例子,笔者认为你肯定很清楚ABA的危害了。
ABA的解决方法
ABA的危害我们清楚了,那么怎么解决ABA呢?
笔者总结了一下,大概有以下几种方式:
(1)版本号
比如,上面的栈结构增加一个版本号用于控制,每次CAS的同时检查版本号有没有变过。
还有一些数据结构喜欢使用高位存储一个邮戳来保证CAS的安全。
(2)不重复使用节点的引用
比如,上面的栈结构在线程2执行push()入栈操作的时候新建一个节点传入,而不是复用节点1的引用;
(3)直接操作元素而不是节点
比如,上面的栈结构push()方法不应该传入一个节点(Node),而是传入元素值(int的value)。
好了,扯了这么多,让我们来看看java中的AtomicStampedReference是怎么解决ABA的吧^^
源码分析
内部类
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
将元素值和版本号绑定在一起,存储在Pair的reference和stamp(邮票、戳的意思)中。
属性
private volatile Pair<V> pair;
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
声明一个Pair类型的变量并使用Unsfae获取其偏移量,存储到pairOffset中。
构造方法
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
构造方法需要传入初始值及初始版本号。
compareAndSet()方法
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
// 获取当前的(元素值,版本号)对
Pair<V> current = pair;
return
// 引用没变
expectedReference == current.reference &&
// 版本号没变
expectedStamp == current.stamp &&
// 新引用等于旧引用
((newReference == current.reference &&
// 新版本号等于旧版本号
newStamp == current.stamp) ||
// 构造新的Pair对象并CAS更新
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
// 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
(1)如果元素值和版本号都没有变化,并且和新的也相同,返回true;
(2)如果元素值和版本号都没有变化,并且和新的不完全相同,就构造一个新的Pair对象并执行CAS更新pair。
可以看到,java中的实现跟我们上面讲的ABA的解决方法是一致的。
首先,使用版本号控制;
其次,不重复使用节点(Pair)的引用,每次都新建一个新的Pair来作为CAS比较的对象,而不是复用旧的;
最后,外部传入元素值及版本号,而不是节点(Pair)的引用。
案例
让我们来使用AtomicStampedReference解决开篇那个AtomicInteger带来的ABA问题。
public class ABATest {
public static void main(String[] args) {
testStamp();
}
private static void testStamp() {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
new Thread(()->{
int[] stampHolder = new int[1];
int value = atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("thread 1 read value: " + value + ", stamp: " + stamp);
// 阻塞1s
LockSupport.parkNanos(1000000000L);
if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) {
System.out.println("thread 1 update from " + value + " to 3");
} else {
System.out.println("thread 1 update fail!");
}
}).start();
new Thread(()->{
int[] stampHolder = new int[1];
int value = atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("thread 2 read value: " + value + ", stamp: " + stamp);
if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) {
System.out.println("thread 2 update from " + value + " to 2");
// do sth
value = atomicStampedReference.get(stampHolder);
stamp = stampHolder[0];
System.out.println("thread 2 read value: " + value + ", stamp: " + stamp);
if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) {
System.out.println("thread 2 update from " + value + " to 1");
}
}
}).start();
}
}
运行结果为:
thread 1 read value: 1, stamp: 1
thread 2 read value: 1, stamp: 1
thread 2 update from 1 to 2
thread 2 read value: 2, stamp: 2
thread 2 update from 2 to 1
thread 1 update fail!
可以看到线程1最后更新1到3时失败了,因为这时版本号也变了,成功解决了ABA的问题。
总结
(1)在多线程环境下使用无锁结构要注意ABA问题;
(2)ABA的解决一般使用版本号来控制,并保证数据结构使用元素值来传递,且每次添加元素都新建节点承载元素值;
(3)AtomicStampedReference内部使用Pair来存储元素值及其版本号;
彩蛋
(1)java中还有哪些类可以解决ABA的问题?
AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,标记值有修改,了解一下。
(2)实际工作中遇到过ABA问题吗?
笔者还真遇到过,以前做棋牌游戏的时候,ABCD四个玩家,A玩家出了一张牌,然后他这个请求迟迟没到服务器,也就是超时了,服务器就帮他自动出了一张牌。
然后,转了一圈,又轮到A玩家出牌了,说巧不巧,正好这时之前那个请求到了服务器,服务器检测到现在正好是A出牌,而且请求的也是出牌,就把这张牌打出去了。
然后呢,A玩家的牌就不对了。
最后,我们是通过给每个请求增加一个序列号来处理的,检测到过期的序列号请求直接抛弃掉。
你有没有遇到过ABA问题呢?
欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。
死磕 java并发包之AtomicStampedReference源码分析(ABA问题详解)的更多相关文章
- 死磕 java并发包之AtomicInteger源码分析
问题 (1)什么是原子操作? (2)原子操作和数据库的ACID有啥关系? (3)AtomicInteger是怎么实现原子操作的? (4)AtomicInteger是有什么缺点? 简介 AtomicIn ...
- 死磕 java并发包之LongAdder源码分析
问题 (1)java8中为什么要新增LongAdder? (2)LongAdder的实现方式? (3)LongAdder与AtomicLong的对比? 简介 LongAdder是java8中新增的原子 ...
- 死磕 java同步系列之CyclicBarrier源码解析——有图有真相
问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...
- 死磕 java同步系列之Phaser源码解析
问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...
- 死磕 java同步系列之StampedLock源码解析
问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...
- 死磕 java同步系列之Semaphore源码解析
问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaph ...
- 死磕 java同步系列之ReentrantReadWriteLock源码解析
问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...
- 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁
问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等 ...
- 【集合框架】JDK1.8源码分析之ArrayList详解(一)
[集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...
随机推荐
- (测试)LaTeX公式
\[a^2+b^2=c^2\] \[F(\omega)=\mathcal{F}[f(t)]=\int_{-\infty}^\infty f(t)e^{-iwt}\,dt\] \[\sum_{i=0}^ ...
- js some和filter用法和区别
some方法 array1.some(callbackfn[, thisArg]) 对数组array1中的每个元素调用回调函数callbackfn,当回调函数返回true或者遍历完所有数组后,some ...
- Win32编程点滴3 - 简单ActiveX控件的使用
虽然这里一片的.net气氛,到处充斥着像MVC.WPF.WorkFlow.LINQ等各种niubility的术语.但我们使用的Windows还是由COM技术主宰着:我们在选择日常使用的软件时,也会避免 ...
- Hadoop中Yarnrunner里面submit Job以及AM生成 至Job处理过程源码解析
参考 http://blog.csdn.net/caodaoxi/article/details/12970993 Hadoop中Yarnrunner里面submit Job以及AM生成 至Job处理 ...
- 调试 Hadoop 源代码
环境是 64bit Ubuntu 14.04 系统, jdk 1.7 以及 Eclipse Mars (4.5) 这里介绍两种调试 Hadoop 源代码的方法: 利用 Eclipse 远程调试工具和打 ...
- [CVE-2017-8464]Microsoft Windows远程命令执行漏洞复现
版权声明:本文为博主的原创文章,未经博主同意不得转载 前言 记录下自己的复现,保留意见 2017年6月13日,微软官方发布编号为CVE-2017-8464的漏洞公告,官方介绍Windows系统在解析快 ...
- UVa 1645 Count (递推,数论)
题意:给定一棵 n 个结点的有根树,使得每个深度中所有结点的子结点数相同.求多棵这样的树. 析:首先这棵树是有根的,那么肯定有一个根结点,然后剩下的再看能不能再分成深度相同的子树,也就是说是不是它的约 ...
- c#重写 重载
重写:当一个子类继承一父类,而子类中的方法与父类中的方法的名称,参数个数.类型都完全一致时,就称子类中的这个方法重写了父类中的方法. 重写:通常,派生类继承基类的方法.因此,在调用对象继承方法的时候, ...
- POJ3191【(-2)进制本质】
题意: 实现10进制数转换成-2进制数 思路: 有点意思,先扯些题外话,一个我们经常做的二进制:利用二进制有好多优化,大多都是利用了二进制能够表示一个数,然后优化了空间或者时间. 所以问题很清楚啊,就 ...
- CodeForces717C 【数学】
题意: 给你n个数既表示a类的值也表示b类的值,然后计算a和b类两两搭配相乘相加,使得答案最小: 思路: 显而易见的方案是最小乘最大,次小乘次大,然后依次下去.. 可以那个特例证明这个是对的 #inc ...