Java 并行无锁递增id生成

Java 并行无锁递增id生成,java,concurrency,parallel-processing,lock-free,Java,Concurrency,Parallel Processing,Lock Free,我有一个映射,它应该将字符串与id相关联。id之间必须有而不是间隙,并且它们必须是从0到N的唯一整数 请求总是带有两个字符串,其中一个、两个或没有可能已经被索引。 映射是从ForkJoin池并行构建的,理想情况下,我希望避免显式同步块。我正在寻找一种最佳的方法来最大限度地提高吞吐量,无论有无锁定 我不知道如何使用AtomicInteger而不为地图中已经存在的键按顺序创建间隙 public class Foo { private final Map<String, Integer&

我有一个映射,它应该将字符串与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
   }
}
公共类Foo{
私有最终映射idGenerator=新ConcurrentHashMap();
//从多个线程调用
公共无效更新(字符串键1、字符串键2){
idGenerator.dosomething(键,?)//应保存键和唯一id
idGenerator.dosomething(键2,?)//应保存键2及其唯一id
Bar Bar=新条(idGenerator.get(键),idGenerator.get(键2));
//…用酒吧做点什么
}
}
我认为
size()
方法与
merge()
相结合可能会解决这个问题,但我不能完全说服自己。有人能提出解决这个问题的方法吗

编辑

关于重复标志,这不能用链接答案中建议的
AtomicInteger.incrementAndGet()
解决。如果我对每个字符串都盲目地这样做,那么序列中就会出现间隙。需要进行复合操作,检查密钥是否存在,然后生成id。 我正在寻找一种通过
Map
API实现这种复合操作的方法


提供的第二个答案与我在问题中明确提出的要求背道而驰。

没有一种方法可以完全按照您想要的方式来做—它本身并不是无锁的。但是,您可以使用该函数以原子方式执行此操作,而无需执行任何显式的锁管理

下面是一个代码示例,它与您提供的样式相同,应该可以帮助您继续

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 keyMap=new ConcurrentHashMap();
AtomicInteger序列=新的AtomicInteger();
公共无效更新(字符串键1、字符串键2){
整数id1=keyMap.computeIfAbsent(key1,s->sequence.getAndIncrement());
整数id2=keyMap.computeIfAbsent(key2,s->sequence.getAndIncrement());
棒材=新棒材(id1、id2);
//…用酒吧做点什么
}

我不确定你能做你想做的事。但是,您可以批处理一些更新,或者在枚举/添加时单独执行检查

很多答案都是假设顺序并不重要:您需要所有字符串都给定一个数字,但即使在一对内重新排序也可以,对吗?并发可能已经导致对的重新排序,或者对的成员无法获得连续的数字,但重新排序可能导致对中的第一个获得更高的数字

延迟并不是那么重要。这个应用程序将处理大量数据并最终生成输出。大多数情况下,地图上应该有搜索结果

如果大多数搜索成功,那么我们主要需要地图上的读取吞吐量

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
   }
}
一个编写器线程就足够了

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

这样,您就不需要原子计数器,或者当两个线程看到相同的字符串时,在将其添加到映射之前将计数器递增两次也没有任何问题。(否则这是个大问题。)

如果有一种方法可以让编写者锁定
ConcurrentHashMap
,从而提高一批更新的效率,那就很好了。但是如果命中率预计会很高,那么您真的希望其他阅读器线程在我们增加它的同时尽可能多地过滤重复项,而不是暂停


为了减少主前端线程之间的争用,可以有多个队列,比如每个线程可能有一个生产者/消费者队列,或者一对物理内核上运行的一组4个线程共享一个队列

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

在读卡器不与写卡器争用的队列中,枚举线程没有争用。但多个队列减少了写入程序之间的争用。(写入这些队列的线程是以只读方式访问主ConcurrentHashMap的线程,如果命中率很高,将花费大部分CPU时间。)


如果Java具有某种数据结构,那么某种数据结构可能是好的。它可以让读者保持全速过滤重复项,同时枚举线程通过一批插入构建一个新表,在构建新表时没有争用


有了90%的命中率,一个writer线程可能可以跟上10个左右的reader线程,根据主表过滤新键

您可能需要设置一些队列大小限制,以允许来自单个writer线程的背压。或者,如果您拥有的内核/线程比单个编写器所能跟上的多得多,那么某种并发设置可能会有帮助,让多个线程在编号之前消除重复

或者说真的,如果你能等到最后把所有的东西都编号,我想这会简单得多


我曾考虑过,在比赛条件下,可能会试图找出错误的空间,然后再回去解决问题,但这可能不会更好。

@Andreas我不认为这是一个重复。这个罐头