无锁并发堆栈实现

时间:2018-11-09 10:54:38

标签: java algorithm concurrency

我一直在研究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;
    }
}

2 个答案:

答案 0 :(得分:1)

是的,您的堆栈有一个ABA问题。

  • 线程A pop进行localTop = top.get()并读取localTop.next

  • 其他线程会弹出一堆东西,然后以不同的顺序放回去,但是线程A的localTop仍然是最后推送的那个。

  • 线程A的CAS成功,但是它破坏了堆栈,因为它从localTop.next读取的值不再准确。

但是,在诸如Java的垃圾收集语言中,

无锁数据结构 要比其他语言更容易实现。如果push()每次都分配一个新的堆栈项,那么您的ABA问题就消失了。然后StackItem.next可以成为最终定论,整个事情变得容易推论。

答案 1 :(得分:1)

您实际上并不需要测试中带有“如果条件”和“ lastItem”的怪异循环,只需弹出并推送同一节点即可重现该错误。

要解决上述问题,您可以在将其推入堆栈时(将现有计数器传递到新的倾斜节点中)创建新的TestStackItem,也可以使用AtomicStampedReference来查看节点是否已被修改。