0

我有一个类序列化器:

class Serializer<T> extends Consumer<T> {
    final Consumer<? super T> actual;
    // constructor omitted for brewity
    @Override public synchronized void accept(T t) {
         actual.accept(t);
    }
}

目的是确保实际一次从单个线程运行。但是,在持有锁的同时调用回调通常是危险的,因此调用者不是持有锁,而是将传入的值排队,其中一个线程将进入,排空队列并依次循环调用实际的使用者。(另一个限制是并发调用者的数量是未知的。)

    final ConcurrentLinkedQueue<T> queue;
    final AtomicInteger wip;
    @Override public void accept(T t) {
        queue.offer(t);
        if (wip.getAndIncrement() == 0) {
            do {
                actual.accept(queue.poll());
            } while (wip.decrementAndGet() > 0);
        }
    }

这是可行的,并且解决了无界队列、线程跳跃和线程卡在循环中的问题,但与直接方法调用相比,基准测试在单线程情况下提供了 10% 的吞吐量。当 I. 使用同步块实现此排队/发射时,基准测试提供了 50% 的直接情况,因为 JVM 优化了同步;这会很棒,但它也不能扩展。使用 juc.Lock 可以扩展,但会遭受与上述代码类似的单线程吞吐量下降。如果我没记错的话,一旦 JVM 优化同步了,它仍然必须使用一些保护,以防该方法再次被并发调用并放回锁定。

我的问题是,如何使用锁、队列或其他序列化逻辑来实现类似的效果,即,在没有并发调用的情况下有一条便宜且快速的路径,并为并发情况提供另一条路径,所以代码可以扩展并且对于单线程使用也保持快速。

4

2 回答 2

1

正如其他人所说,synchronized已经为您提供了一个相当有效的工具。因此,使用不同的东西不应该是为了获得更好的性能。

但是,如果您的意图不是像您在问题中所说的那样阻止来电者,那是合法的。(尽管这可能意味着性能较差)。

在查看您使用队列和原子整数的尝试时,我看到的第一件事是,在没有待处理项目且没有其他消费者运行的情况下,可以绕过队列。在可能减少开销的低争用情况下:

final ConcurrentLinkedQueue<T> queue;
final AtomicInteger wip;
@Override public void accept(T t) {
  if(wip.compareAndSet(0, 1)) { // no contention?
    actual.accept(t);
    if(wip.decrementAndGet()==0) return; // still no contention
  }
  else {
    if(!queue.offer(t))
      throw new AssertionError("queue should be unbounded");
    if(wip.getAndIncrement() != 0) return; // other consumer running
  }
  do {
    actual.accept(queue.poll());
  } while (wip.decrementAndGet() > 0);
}
于 2014-05-19T10:20:46.717 回答
0

这就是 synchronized 已经为您所做的。(它在其他策略中使用偏向锁定)

如果您要问是否有更有效的方法来以一般方式同步数据,那么如果您能想到该领域数十年的专业知识,我会感到非常惊讶。如果您确实找到了一种可以让您出名的更快方法,那可能是“kd304 锁定策略”。

如果您问是否有办法减少特定/特殊情况代码段的开销,那么肯定有,但这取决于您在做什么,这从问题中不清楚。

于 2014-05-18T17:26:29.730 回答