繁体   English   中英

并行无锁升序id生成

[英]Parallel lock-free ascending id generation

我有一个地图,应该将字符串与ID相关联。 id之间一定不能有间隙,它们必须是从0到N的唯一整数。

请求总是带有两个字符串,其中一个,两个或没有可能已被索引。 地图是从ForkJoin池并行构建的,理想情况下我想避免显式同步块。 我正在寻找一种最佳方法来最大化吞吐量,无论是否锁定。

我没有看到如何使用AtomicInteger而不会为已经存在于地图中的键创建序列间隙。

public class Foo {
    private final Map<String, Integer> idGenerator = new ConcurrentHashMap<>();

    // invoked from multiple threads
    public void update(String key1, String key2) {
      idGenerator.dosomething(key, ?) // should save the key and unique id
      idGenerator.dosomething(key2, ?) // should save the key2 and its unique id
      Bar bar = new Bar(idGenerator.get(key), idGenerator.get(key2));
      // ... do something with bar
   }
}

我认为size()方法结合merge()可能会解决问题,但我不能完全说服自己。 有谁能建议解决这个问题的方法?

编辑

关于重复标志,这不能通过链接答案中建议的AtomicInteger.incrementAndGet()来解决。 如果我盲目地为每个字符串执行此操作,则序列中将存在间隙 需要复合操作来检查密钥是否存在,然后才生成id。 我一直在寻找一种通过Map API实现这种复合操作的方法。

第二个提供的答案违背了我在问题中特别提出的要求。

没有办法按照你想要的方式完成它 - ConcurrentHashMap本身并不是无锁的。 但是,您可以通过原子方式执行此操作,而无需使用java.util.Map.computeIfAbsent函数进行任何显式锁定管理。

这是一个代码示例,其风格与您提供的内容有关。

ConcurrentHashMap<String, Integer> keyMap = new ConcurrentHashMap<>();
AtomicInteger sequence = new AtomicInteger();

public void update(String key1, String key2) {
    Integer id1 = keyMap.computeIfAbsent(key1, s -> sequence.getAndIncrement());
    Integer id2 = keyMap.computeIfAbsent(key2, s -> sequence.getAndIncrement());

    Bar bar = new Bar(id1, id2);
    // ... do something with bar
}

我不确定你能做到你想要的。 但是,您可以批量处理某些更新,或者与枚举/添加分开进行检查。

很多答案都假设订单并不重要:你需要给出一个数字的所有字符串,但即使在一对中也可以重新排序,对吧? 并发可能已经导致对的重新排序,或者对于成员的成员不能获得连续的数字,但重新排序可能导致一对中的第一个获得更高的数字。

延迟并不重要。 该应用程序应该咀嚼大量数据并最终产生输出。 大多数情况下,应该在地图中进行搜索

如果大多数搜索命中,那么我们通常需要在地图上读取吞吐量。

单个编写器线程可能就足够了。

因此,不是直接添加到主映射,并发读者可以检查他们的输入,如果不存在,则将它们添加到要枚举的队列并添加到主ConcurrentHashMap。 队列可以是一个简单的无锁队列,也可以是另一个ConCurrentHashMap,也可以从尚未添加的候选中过滤重复项。 但是可能无锁队列很好。

然后你不需要一个原子计数器,或者当两个线程在它们中的任何一个可以将它添加到地图之前看到相同的字符串时两个线程递增计数器有任何问题。 (因为否则这是个大问题。)

如果有一种方法可以让编写器锁定ConcurrentHashMap以使一批更新更有效,那可能会很好。 但是如果预计命中率非常高,那么你真的希望其他读者线程尽可能多地过滤重复数据,而我们正在增长而不是暂停它。


为了减少主要前端线程之间的争用,您可以拥有多个队列,例如每个线程可能具有单生产者/单个使用者队列,或者在一对物理核心上运行的一组4个线程共享一个队列。

枚举线程从所有这些中读取。

在读者不与编写者竞争的队列中,枚举线程没有争用。 但是多个队列可以减少编写者之间的争用。 (编写这些队列的线程是以只读方式访问主ConcurrentHashMap的线程,如果命中率很高,则会占用大部分CPU时间。)


如果Java有这种结构,那么某种读取 - 复制 - 更新(RCU)数据结构可能会很好 它可以让读者继续全速过滤重复项,而枚举线程构建一个新表,并完成一批插入,在构建新表时没有争用。


命中率达到90%时,一个编写器线程可能会跟上10个左右的读取器线程,这些线程会根据主表过滤新的键。

您可能希望设置一些队列大小限制以允许来自单个写入器线程的反压。 或者,如果你有更多的内核/线程,而不是单个编写器可以跟上,那么可能某种并发设置让多个线程在编号之前消除重复是有帮助的。

或者真的,如果你可以等到最后给所有东西编号,那我觉得这会简单得多。

我想过可能会尝试在竞争条件下为错误编号,然后回去解决问题,但这可能并不是更好。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM