我在Wikipedia的实践书中研究了并发中的ABA问题,并在阅读post之后
据我了解ABA问题的根本原因,我们在算法中检查状态与以前相同,但是算法暗示状态未受影响.
具有堆栈数据结构的示例:
要添加元素到堆栈,我们使用以下算法:
create new stack node(save to `newNode` variable)
while(true) {
oldHead = stack.get();
newNode.next = oldHead; // point_1
if(stack.compareAndSet(oldhead, newNode)) { // atomically replace head if now head same as was in start of iteration
break;
}
}
导致ABA问题的步骤:
初始状态
a->b->c // a-head, c- tail.
> Thread_1尝试将值d添加到堆栈中,并且OS在compareAndSet操作(point_1)之前挂起线程
>线程_2,然后执行弹出(线程_1仍处于暂停状态)
b->c // b-head, c- tail.
> Thread_3然后执行弹出(Thread_1仍然挂起)
c // c-head, c- tail.
> Thread_4然后执行push a(Thread_1仍然挂起)
a->c // a-head, c- tail.
>线程_1唤醒并且cas操作成功执行,尽管在某些情况下可能不希望这样做.
尽管this answer被接受,但我不明白为什么自动垃圾收集可以消除此问题.
尽管我不是C语言方面的专家,但我理解C语言中您不能为两个不同的对象分配一个内存范围.
您能否更清楚地澄清它?
解决方法:
造成混淆的部分原因可能是您将数据结构本身与内容混淆了.
在“适当”的实现中,您将拥有包含数据的堆栈节点,而不是数据本身.因此,最终得到的是Node(a),Node(b)等-因此,当某个线程将c推入堆栈时,它将传递c,但实际上是被推入的Node(c).
这意味着,在这种环境下,在步骤4中进入堆栈的对象将不仅仅是a,而是新的Node(a),它将与其他线程在步骤1中看到的Node(a)指针不同.这些对象在业务方面可能非常相等(例如,在java中,它们equals方法将返回true),但是指针之间的比较将有所不同.
在这里,我们谈到自动GC有所作为的地方.第1步中被阻塞的线程仍然保留对堆栈/寄存器上Node(a)的旧实例的引用,即使稍后将其从堆栈中删除,并且在堆中的任何地方也没有强引用.这样可以防止该节点被删除以及内存地址被重新使用,而这将使CAS陷于瘫痪.
请注意,如果您在线程1仍处于关键部分的情况下删除(在内存方面)原始Node(a),则您在此处指的糟糕情况只会在非GC语言中发生.这是非常棘手的-您让线程留有指向释放的内存块的指针,并且需要非常确实地确保它不会在以后的任何时候被取消引用,因为这样最终会导致ABA产生更糟糕的结果(您可以阅读来自释放块的任何垃圾).
如果您没有以Node(x)的形式实现间接层,而只是让客户端调用直接推送/弹出/修改内部节点,那么所有的赌注都将不起作用-例如,客户端可以将同一节点推送两次,从而导致无限稍后循环.在您的示例中,它将首先删除,然后再重新插入同一节点,但是这假设数据结构和客户端代码之间存在很多泄漏状态-在多线程环境中做这是非常危险的事情,尤其是如果您想尝试无锁结构.
综上所述,自动GC不能保护所有情况下的ABA.对于总括性优化的无锁代码(保留对死对象的引用),它可以防止ABA的特定情况(与malloc的内存重用有关).