【问题标题】:Bounded, auto-discarding, non-blocking, concurrent collection有界、自动丢弃、非阻塞、并发收集
【发布时间】:2011-04-29 12:39:03
【问题描述】:

我正在寻找一个合集:

  • 是一个Deque/List - 即支持在“顶部”插入元素(最新项目位于顶部) - deque.addFirst(..) / list.add(0, ..)。可能是 Queue,但迭代顺序应该是相反的 - 即最近添加的项目应该排在第一位。
  • 有界 - 即限制为 20 个项目
  • 达到容量时自动丢弃最旧的项目(那些“在底部”,首先添加)
  • 非阻塞 - 如果双端队列为空,则检索不应阻塞。它也不应该阻塞/返回false/null/抛出异常是双端队列已满。
  • 并发 - 多个线程应该能够对其进行操作

我可以将LinkedBlockingDeque 包装到我的自定义集合中,在add 操作中检查大小并丢弃最后一个项目。有更好的选择吗?

【问题讨论】:

  • "非阻塞 - 如果双端队列为空,则检索不应阻塞。它也不应返回 null / 如果双端队列已满,则抛出异常。" - 从空列表中检索项目时应该发生什么?既不异常,也不阻塞,也不返回null?
  • 在检索null 时可以返回。如果双端队列已满,则应添加该项目,而旧项目 - 丢弃
  • 只是说明 LinkedBlockingDeque 不是并发的。
  • 我认为你说的是​​队列而不是双端队列。

标签: java collections


【解决方案1】:

我相信您正在寻找的是有界堆栈。没有一个核心库类可以做到这一点,所以我认为最好的方法是采用非同步堆栈(LinkedList)并将其包装在一个同步集合中,该集合执行自动丢弃并在空时返回 null流行音乐。像这样的:

import java.util.Iterator;
import java.util.LinkedList;

public class BoundedStack<T> implements Iterable<T> {
    private final LinkedList<T> ll = new LinkedList<T>();
    private final int bound;

    public BoundedStack(int bound) {
        this.bound = bound;
    }

    public synchronized void push(T item) {
        ll.push(item);
        if (ll.size() > bound) {
            ll.removeLast();                
        }
    }

    public synchronized T pop() {
        return ll.poll();
    }

    public synchronized Iterator<T> iterator() {
        return ll.iterator();
    }
}

...根据需要添加诸如 isEmpty 之类的方法,如果您希望它实现例如 List。

【讨论】:

  • 您应该考虑使用 ReentrantLock 而不是 synchronized 关键字,这样您的锁对象就不能公开使用。
【解决方案2】:

我做了这个简单的实现:

public class AutoDiscardingDeque<E> extends LinkedBlockingDeque<E> {

    public AutoDiscardingDeque() {
        super();
    }

    public AutoDiscardingDeque(int capacity) {
        super(capacity);
    }

    @Override
    public synchronized boolean offerFirst(E e) {
        if (remainingCapacity() == 0) {
            removeLast();
        }
        super.offerFirst(e);
        return true;
    }
}

对于我的需要,这已经足够了,但它应该是与 addFirst / offerFirst 不同的有据可查的方法仍然遵循阻塞双端队列的语义。

【讨论】:

  • 你需要同步你使用的方法,否则结果不是线程安全的——多个线程可能同时运行offerFirst,导致奇怪的结果。
【解决方案3】:

最简单和经典的解决方案是覆盖最旧元素的有界环形缓冲区。

实现相当简单。您需要一个 AtomicInteger/Long 用于索引 + AtomicReferenceArray,并且您有一个无锁通用堆栈,只有两种方法 offer/poll,没有 size()。大多数并发/无锁结构都有 size() 的困难。非覆盖堆栈可以有 O(1),但在 put 上有分配。

类似的东西:

package bestsss.util;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReferenceArray;

public class ConcurrentArrayStack<E> extends AtomicReferenceArray<E>{
    //easy to extend and avoid indirections, 
    //feel free to contain the ConcurrentArrayStack if feel purist


    final AtomicLong index = new AtomicLong(-1);

    public ConcurrentArrayStack(int length) {
        super(length);  //returns           
    }

    /**
     * @param e the element to offer into the stack
     * @return the previously evicted element
     */
    public E offer(E e){
        for (;;){
            long i = index.get();
            //get the result, CAS expect before the claim
            int idx = idx(i+1);
            E result = get(idx);

            if (!index.compareAndSet(i, i+1))//claim index spot
                continue;

            if (compareAndSet(idx, result, e)){
                return result;
            }
        }
    }

    private int idx(long idx){//can/should use golden ratio to spread the index around and reduce false sharing
        return (int)(idx%length());
    }

    public E poll(){
        for (;;){
            long i = index.get();
            if (i==-1)
                return null;

            int idx = idx(i);
            E result = get(idx);//get before the claim

            if (!index.compareAndSet(i, i-1))//claim index spot
                continue;

            if (compareAndSet(idx, result, null)){
                return result;
            }
        }
    }
}

最后一点: 进行 mod 操作是一项昂贵的操作,并且首选 2 的幂容量,通过&amp;length()-1(也可以保护与长溢出)。

【讨论】:

  • 太棒了! (还有 8 个……)
【解决方案4】:

这是一个处理并发并且从不返回 Null 的实现。

import com.google.common.base.Optional;

import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.locks.ReentrantLock;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

public class BoundedStack<T> {
   private final Deque<T> list = new ConcurrentLinkedDeque<>();
   private final int maxEntries;
   private final ReentrantLock lock = new ReentrantLock();

   public BoundedStack(final int maxEntries) {
      checkArgument(maxEntries > 0, "maxEntries must be greater than zero");
      this.maxEntries = maxEntries;
   }

   public void push(final T item) {
      checkNotNull(item, "item must not be null");

      lock.lock();
      try {
         list.push(item);
         if (list.size() > maxEntries) { 
            list.removeLast();
         }
      } finally {
         lock.unlock();
      }
   } 

   public Optional<T> pop() {
      lock.lock();
      try {
         return Optional.ofNullable(list.poll());
      } finally {
         lock.unlock();
      }
   }

   public Optional<T> peek() {
      return Optional.fromNullable(list.peekFirst());
   }

   public boolean empty() {
      return list.isEmpty();
   }
}

【讨论】:

  • 没有人对此解决方案发表任何评论,但它实际上是最现代的:它使用线程安全的ConcurrentLinkedDeque,它不返回null,而是返回Optional。或许,5 年后我能提出的唯一建议是用 Java 8 替换 Guava 的 Optional。好东西!
【解决方案5】:

对于@remery 给出的解决方案,如果另一个线程在该时间段内运行pop() 并且列表现在在容量范围内,您是否会遇到在if (list.size() &gt; maxEntries) 之后您可能会错误地删除最后一个元素的竞争条件。鉴于pop()public void push(final T item) 之间没有线程同步。

对于@Bozho 给出的解决方案,我认为可能会出现类似的情况?同步发生在AutoDiscardingDeque 上,而不是LinkedBlockingDeque 内部的ReentrantLock,所以在运行remainingCapacity() 后,另一个线程可以从列表中删除一些对象,而removeLast() 会删除一个额外的对象?

【讨论】:

  • 我同意存在潜在的竞争条件。 pop 也应该受到锁的保护。
猜你喜欢
  • 2011-12-14
  • 1970-01-01
  • 2018-11-15
  • 2012-08-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-30
  • 1970-01-01
相关资源
最近更新 更多