带翻转缓冲器的无锁容器

时间:2019-03-10 14:49:19

标签: java multithreading lock-free

对于我的一个必须支持并发读取和写入的项目,我需要一个能够缓冲项目的容器,直到使用者一次获取每个当前缓冲的项目为止。由于生产者应该能够产生数据,而不管消费者是否读取当前缓冲区,所以我想出了一个自定义实现,该实现在AtomicReference的帮助下将每个条目添加到支持ConcurrentLinkedQueue直到翻转执行该操作会导致当前条目被返回,同时存储带有空队列的新条目,并且元数据将被原子地存储在该AtomicReference中。

我想出了一个解决方案,例如

public class FlippingDataContainer<E> {

  private final AtomicReference<FlippingDataContainerEntry<E>> dataObj = new AtomicReference<>();

  public FlippingDataContainer() {
    dataObj.set(new FlippingDataContainerEntry<>(new ConcurrentLinkedQueue<>(), 0, 0, 0));
  }

  public FlippingDataContainerEntry<E> put(E value) {
    if (null != value) {
      while (true) {
        FlippingDataContainerEntry<E> data = dataObj.get();
        FlippingDataContainerEntry<E> updated = FlippingDataContainerEntry.from(data, value);
        if (dataObj.compareAndSet(data, updated)) {
          return merged;
        }
      }
    }
    return null;
  }

  public FlippingDataContainerEntry<E> flip() {
    FlippingDataContainerEntry<E> oldData;
    FlippingDataContainerEntry<E> newData = new FlippingDataContainerEntry<>(new ConcurrentLinkedQueue<>(), 0, 0, 0);
    while (true) {
      oldData = dataObj.get();
      if (dataObj.compareAndSet(oldData, newData)) {
        return oldData;
      }
    }
  }

  public boolean isEmptry() {
    return dataObj.get().getQueue().isEmpty();
  }
}

由于需要将当前值推送到后备队列,因此现在需要特别注意。 from(data, value)方法的当前实现确实看起来像这样:

static <E> FlippingDataContainerEntry<E> from(FlippingDataContainerEntry<E> data, E value) {
  Queue<E> queue = new ConcurrentLinkedQueue<>(data.getQueue());
  queue.add(value);
  return new FlippingDataContainerEntry<>(queue,
      data.getKeyLength() + (value.getKeyAsBytes() != null ? value.getKeyAsBytes().length : 0),
      data.getValueLength() + (value.getValueAsBytes() != null ? value.getValueAsBytes().length : 0),
      data.getAuxiliaryLength() + (value.getAuxiliaryAsBytes() != null ? value.getAuxiliaryAsBytes().length : 0));
}

由于另一个线程在此线程执行更新之前更新了值可能导致重试,因此我需要在每次写入尝试时复制实际队列,因为否则即使原子操作也会将条目添加到共享队列中参考无法更新。因此,仅将值添加到共享队列中可能导致将值条目实际上仅应出现一次而多次添加到队列中。

复制整个队列是一项非常昂贵的任务,因此,我只考虑设置当前队列,而不是在from(data, value)方法中复制队列,而不是将value元素添加到执行的块中的共享队列中更新发生的时间:

public FlippingDataContainerEntry<E> put(E value) {
  if (null != value) {
    while (true) {
      FlippingDataContainerEntry<E> data = dataObj.get();
      FlippingDataContainerEntry<E> updated = FlippingDataContainerEntry.from(data, value);
      if (data.compareAndSet(data, updated)) {
        updated.getQueue().add(value);
        return updated;
      }
    }
  }
  return null;
}

from(data, value)内,我现在只设置队列而没有直接添加value元素

static <E> FlippingDataContainerEntry<E> from(FlippingDataContainerEntry<E> data, E value) {
  return new FlippingDataContainerEntry<>(data.getQueue(),
      data.getKeyLength() + (value.getKeyAsBytes() != null ? value.getKeyAsBytes().length : 0),
      data.getValueLength() + (value.getValueAsBytes() != null ? value.getValueAsBytes().length : 0),
      data.getAuxiliaryLength() + (value.getAuxiliaryAsBytes() != null ? value.getAuxiliaryAsBytes().length : 0));
}

尽管与复制队列的代码相比,此方法可以使测试运行速度提高10倍以上,但它也经常导致消耗测试失败,因为现在,在消费者线程翻转了值之后,可能会在队列中添加value元素排队并处理了数据,因此并不是所有物品都被消耗掉了。

现在的实际问题是,是否可以避免复制备份队列以提高性能,同时仍然允许使用无锁算法自动更新队列的内容,因此也可以避免途中丢失某些条目?

1 个答案:

答案 0 :(得分:1)

首先,让我们说明一个显而易见的问题-最佳解决方案是避免编写任何此类自定义类。也许像java.util.concurrent.LinkedTransferQueue这样简单的东西也可以正常工作,并且不易出错。如果LinkedTransferQueue不起作用,那么LMAX disruptor或类似的东西怎么办?您是否看过现有的解决方案?

如果您仍然需要/想要一种自定义解决方案,那么我略略介绍了一种方法,该方法可以避免复制:

这个想法是让put操作旋转一些原子变量,尝试对其进行设置。如果线程设法对其进行设置,那么它将获得对当前队列的独占访问权,这意味着它可以追加到该队列。追加后,它将重置原子变量以允许其他线程追加。基本上是spin-lock。这样,线程之间的争用发生在附加到队列之前,而不是之后。