【问题标题】:Read ConcurrentHashMap (or similar) in chunks分块读取 ConcurrentHashMap(或类似的)
【发布时间】:2016-01-09 00:47:30
【问题描述】:

我有一个程序,它有一个ConcurrentHashMap,不同的线程可以在其中添加/删除地图中的项目。

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

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

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

更新:我也没有绑定到 ConcurrentHashMap 。我只是在寻找最好的解决方案

更新 2:它们的键是 String

谢谢

【问题讨论】:

  • 我会担心用ConcurrentHashMap 来做这件事,因为a)它没有固定的顺序,b)它可以反映或不能反映迭代开始后对地图所做的更改,在一个未定义的方式。这似乎很可能以糟糕的方式结束。老实说,让用户与一组同时被其他线程修改的项目进行交互似乎很可能会以糟糕的方式结束。
  • @LouisWasserman 是的,当我开始从事该项目时,不需要此功能,而 ConcurrentHashMap 是最佳解决方案。这就是为什么我说我愿意接受任何解决方案。
  • 地图上进行了哪些操作,能一一列举吗?什么是“项目”——键、值或条目?编辑:可以将项目重新添加到地图中吗?如果可以,是否必须显示两次/仅第一次,或者没有区别?
  • @user3707125 操作是基于键添加/删除条目。项目=条目(我的坏)。是的,可以添加、删除具有特定键的条目,而不是稍后重新添加。条目必须只显示一次,但这不是问题,一旦在后端删除条目,我可以设法从前端进行删除部分,这样我就可以避免重复
  • 当您谈论“按钮点击”时,听起来至少值得考虑为这次锁定数据。读取 25 个元素需要几微秒,当它是由用户交互引起的时,它不会以高频率发生。因此,这不应该显着/负面影响您想要允许的并发性。

标签: java concurrency hashmap java.util.concurrent


【解决方案1】:

在您的情况下,由于字符串键是严格排序的,ConcurrentSkipListMap 是一种方法。

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

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

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

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


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

【讨论】:

  • 这将如何工作?假设一个线程每秒添加 2 个随机字符串值的键,一个线程每秒删除 1 个随机键,每 30 秒我们拉出 25 个,这应该是以前看不到的。如果您在字符串上排序,则有可能(甚至可能)在最后一个键前面添加不超过 25 个键。从最后一个键迭代然后失败?
  • @KoosGadellaa 此机制提供与Iterator 并发集合相同的保证。遍历是weekly consistent,即1)它可以与其他操作同时进行; 2)它从不抛出 ConcurrentModificationException 或类似的; 3)保证遍历元素,因为它们在构造时就存在一次,并且可以反映构造后的任何修改。
  • 我的意思是说'从最后一个键迭代然后产生少于 25 个新结果,即使有超过 25 个看不见的结果'迭代器是在尾集之后创建的,并且跳过列表仍然命令按键,而不是插入顺序(这是必要的)。简单地说:我在地图中的最后一个键是“Zulu”,我的 lastSeenKey 也是“Zulu”,然后另一个线程将“Amsterdam”、“Brussels”、“Copenhagen”添加为新键(具有适当的值)。 map.tailMap('Zulu', false) 不会确保我永远不会看到这些新的看不见的值吗?
  • @KoosGadellaa 这样的迭代器要么返回一整页的不可见元素,要么到达末尾,这意味着迭代器第一次构造中存在的所有元素都已被访问。通常,这是从用户体验角度来看的预期行为。用户在浏览页面时希望有一些一致的顺序,他们通常不希望陷入反复添加和删除的项目的无限循环。
  • 所以它最终会“终止”,因为它无法显示任何元素,因为它的索引远远低于项目,没有添加任何新内容......即使可能添加了无数新项目.
【解决方案2】:

如果您不想使用 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 将永远不会在新批次中显示。

【讨论】:

  • 据我所知,linkedhashmap 不是线程安全的。我错了吗?
猜你喜欢
  • 2014-02-28
  • 2013-04-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-27
相关资源
最近更新 更多