我一直在研究Java中无锁堆栈的简单实现。
编辑:请参见下面的固定/工作版本
您发现此实现有任何问题吗?
用the ABA problem用本国语言进行的类似实现似乎很痛苦,但是我不确定这是否是一个问题;显然,没有直接在Java中直接完成指针处理,并且我所关心的只是弹出和推入的堆栈结束,所以我看不到如何“遗漏”堆栈中任何非尾元素的任何更改都会导致问题
public class LockFreeStack<T extends LockFreeStack.StackItem<T>>
{
public abstract static class StackItem<SELF extends StackItem<SELF>>
{
volatile SELF next;
// .. data ..
}
final AtomicReference<T> top = new AtomicReference<T>(null);
public void push(T item)
{
T localTop;
do {
localTop = top.get();
item.next = localTop;
} while(!top.compareAndSet(localTop, item));
}
public T pop()
{
T localTop;
do {
localTop = top.get();
} while(localTop != null && !top.compareAndSet(localTop, localTop.next));
return localTop;
}
}
但是,这就是我没有得到的。我编写了一个简单的测试,它启动了几个线程。每个项目都会从一个预先存在的LockFreeStack中弹出项目,然后(从弹出该项目的同一线程中)将它们推回。 弹出它之后,我增加一个原子计数器,然后推回它之前,我递减它。因此,我总是希望计数器为0(在递减之后/在推回堆栈之前)或1(在弹出并递增之后)。
但是,那不会发生...
public class QueueTest {
static class TestStackItem extends LockFreeStack.StackItem<TestStackItem>
{
final AtomicInteger usageCount = new AtomicInteger(0);
public void inc() throws Exception
{
int c = usageCount.incrementAndGet();
if(c != 1)
throw new Exception(String.format("Usage count is %d; expected %d", c, 1));
}
public void dec() throws Exception
{
int c = usageCount.decrementAndGet();
if(c != 0)
throw new Exception(String.format("Usage count is %d; expected %d", c, 0));
}
}
public final LockFreeStack<TestStackItem> testStack = new LockFreeStack<TestStackItem>();
public void test()
{
final int NUM_THREADS = 4;
for(int i = 0; i < 10; i++)
{
TestStackItem item = new TestStackItem();
testStack.push(item);
}
Thread[] threads = new Thread[NUM_THREADS];
for(int i = 0; i < NUM_THREADS; i++)
{
threads[i] = new Thread(new TestRunner());
threads[i].setDaemon(true);
threads[i].setName("Thread"+i);
threads[i].start();
}
while(true)
{
Thread.yield();
}
}
class TestRunner implements Runnable
{
@Override
public void run() {
try {
boolean pop = false;
TestStackItem lastItem = null;
while (true) {
pop = !pop;
if (pop) {
TestStackItem item = testStack.pop();
item.inc();
lastItem = item;
} else {
lastItem.dec();
testStack.push(lastItem);
lastItem = null;
}
}
} catch (Exception ex)
{
System.out.println("exception: " + ex.toString());
}
}
}
}
引发非确定性异常,例如
exception: java.lang.Exception: Usage count is 1; expected 0
exception: java.lang.Exception: Usage count is 2; expected 1
或其他运行
exception: java.lang.Exception: Usage count is 2; expected 0
exception: java.lang.Exception: Usage count is 3; expected 1
exception: java.lang.Exception: Usage count is 3; expected 1
exception: java.lang.Exception: Usage count is 2; expected 1
所以某些竞赛条件(例如问题)必须在这里进行。
这是怎么回事-这确实与ABA相关(如果这样,到底是什么?),或者我还有其他遗漏吗?
谢谢!
注意:这行得通,但这似乎不是一个很好的解决方案。它是无垃圾的(StampedAtomicReference在内部创建对象),而无锁的好处似乎并没有得到回报。在我的基准测试中,这在单线程环境中并没有真正快,并且在同时测试6个线程时,它远远落后于仅对push / pop函数进行锁定
根据以下建议的解决方案,这确实是ABA问题,而这一小的更改将规避以下问题:
public class LockFreeStack<T extends LockFreeStack.StackItem<T>>
{
public abstract static class StackItem<SELF extends StackItem<SELF>>
{
volatile SELF next;
// .. data ..
}
private final AtomicStampedReference<T> top = new AtomicStampedReference<T>(null, 0);
public void push(T item)
{
int[] stampHolder = new int[1];
T localTop;
do {
localTop = top.get(stampHolder);
item.next = localTop;
} while(!top.compareAndSet(localTop, item, stampHolder[0], stampHolder[0]+1));
}
public T pop()
{
T localTop;
int[] stampHolder = new int[1];
do {
localTop = top.get(stampHolder);
} while(localTop != null && !top.compareAndSet(localTop, localTop.next, stampHolder[0], stampHolder[0]+1));
return localTop;
}
}
答案 0 :(得分:1)
是的,您的堆栈有一个ABA问题。
线程A pop
进行localTop = top.get()
并读取localTop.next
其他线程会弹出一堆东西,然后以不同的顺序放回去,但是线程A的localTop
仍然是最后推送的那个。
线程A的CAS成功,但是它破坏了堆栈,因为它从localTop.next
读取的值不再准确。
无锁数据结构 要比其他语言更容易实现。如果push()每次都分配一个新的堆栈项,那么您的ABA问题就消失了。然后StackItem.next
可以成为最终定论,整个事情变得容易推论。
答案 1 :(得分:1)
您实际上并不需要测试中带有“如果条件”和“ lastItem”的怪异循环,只需弹出并推送同一节点即可重现该错误。
要解决上述问题,您可以在将其推入堆栈时(将现有计数器传递到新的倾斜节点中)创建新的TestStackItem,也可以使用AtomicStampedReference来查看节点是否已被修改。