分块读取 ConcurrentHashMap(或类似的)

Read ConcurrentHashMap (or similar) in chunks

我有一个程序,它有一个 ConcurrentHashMap,其中不同的线程可以 add/remove 地图中的项目。

我很想知道以 25 项为单位阅读地图的最佳方法是什么。 我想要做的是这样的:用户点击一个按钮并从地图上读取 25 个项目(顺序无关紧要)。之后他可以单击 "Next" 按钮并阅读另外 25 个项目(与前 25 个不同),依此类推。

我不确定我是否可以用 ConcurrentHashMap 做到这一点。我不想使用数据库,我想将其保存在内存中。 我不认为将 Map 转换为 ArrayList 会有帮助,因为大部分时间都是从地图中添加/删除项目。

我愿意接受任何解决方案,甚至是第 3 方库。

更新:我也不受 ConcurrentHashMap 的束缚。我只是在寻找最好的解决方案

更新 2:它们的键是 String

谢谢

对于您的情况,由于字符串键是严格排序的,因此 ConcurrentSkipListMap 是一种可行的方法。

  • 它既是concurrent and navigable,又可以经常用来代替ConcurrentHashMap
  • get / put / removeO(log N).
  • 一样快
  • 作为奖励,您将免费获得自然遍历顺序。

要从 ConcurrentSkipListMap 获取下一页,调用 tailMap 以上一页的最后一个键作为锚点,然后从结果子映射构造迭代器或流:

    return map.tailMap(lastKey, false).entrySet()
            .stream()
            .limit(pageSize)
            .collect(Collectors.toList());

注意 tailMap 即使从地图中删除锚键也会成功。迭代将从下一个大于锚点的键开始。


如果键没有严格排序,或者如果需要 O(1) 复杂度,那么您可能更喜欢替代建议 - 一个开放寻址散列 table 按单元格索引的顺序遍历。然而,标准 Java 类 中没有这样的实现。这种映射的线程安全通常是通过锁定来实现的。

如果不想使用concurrenthashmap,可以考虑使用linkedhashmap,然后同步。
linkedhashmap 按插入顺序存储,因此您可以对其进行迭代。 但是,您需要保留一个 lastViewedElement,当它被删除时,用一个新的有效值更新它,以便您可以从那里进行迭代。

它应该正确同步(可能是手动),因为它不是线程安全的。粗略的实现是:
(我是用头脑做的,而不是 IDE,所以它可能会遗漏一些东西。而且我的泛型不合格,而且你可能根本不需要泛型......)

public class MyMagicMap<X,Y> implements Map
    private LinkedHashMap<X,Y> innerMap = new LinkedHashMap<>();
    X lastKey = null;

    @Override
    public void put(X key,Y value) {
        synchronized(MyMagicMap.class) {
            if(innerMap.contains(key)) {
                innerMap.remove(key);
            }
            innerMap.put(key, value);
        }
    }

    @Override
    public Y remove(X key) {
        synchronized(MyMagicMap.class) {
            if(key.equals(lastKey) {
                lastKey = getKeyBefore(lastKey);
            }
            return innerMap.remove(key);
        }
    }

    private X getKeyBefore(X oldLastKey) {
        Iterator<X> it = innerMap.KeySet.iterator();
        for(X key : it  ) {
            if(key.equals(oldLastKey)) {
                return it.next();
            }
        }
    }

    public Map<X,Y> getNextBatch(int count) {
         synchronized(MyMagicMap.class) {

             Map<X,Y> resultMap = new HashMap();
             Iterator<X> it = innerMap.keySet().iterator();
             X lastKey = scanForLastKey(it);
             if(lastKey == null) {
                 return resultMap;
             }
             resultMap.put(lastKey, innerMap.get(key);
             for(int i; i < count-1; i++) {
                 if(it.hasNext()) {
                     X key = it.next();
                     resultMap.put(X, innerMap.get(key));
                 }
             }
        }
    }

    private X scanForLastKey(Iterator<X> iterator) {
        if(lastKey == null) {
            iterator.next();
        }
        while(iterator.hasNext()) {
            X key = iterator.next();
            if(key.equals(lastKey)) {
                return key;
            }
        }
        return null;
    }




    // other map methods can probably go without synchronization
}

需要注意的是相关块上有线程同步,只有一个线程应该这样做。
另请注意,删除方法性能不佳 (O(N))
如果你想更快地完成它,你必须实现你自己的 LinkedHashMap ,它允许你有效地确定下一个行。听起来很难,直到您意识到您只需要一个 HashMap> 以及具有上一个和下一个键值的三元组。再次同步 put 和 remove 操作,这是为了保持列表的完整性,即

void put(X key, Y value) {
    // synchronize this
    Triplet<X,Y,X> lastValueInMap = innerMap.get(endOfMapKey);
    lastValueInMap.rightElement = key;
    innerMap.put(X, new Triplet(endOfMapKey, value, null);
    endOfMapKey = key;
}
Y remove(X key) {
    // synchronize this.
    Triplet<X,Y,X> valueTriplet = innerMap.remove(key);
    if(valueTriplet.left != null)
        if(valueTriplet.right != null) {
            innerMap.get(valueTriplet.left).right = valueTriplet.right;
            innerMap.get(valueTriplet.right).left=valueTriplet.left;
        } else
        // etc.
     }
}

最后要注意的是 put 方法,如果键已经存在,它会显式删除键,而不是更新值。由于 LinkedHashMap 的更新不会修改插入顺序,这意味着如果未完成删除,任何已在批次中读取并稍后更新的键 K 将永远不会显示在较新的批次中。