【问题标题】:Java concurrency with a Map of Lists带有列表映射的 Java 并发
【发布时间】:2011-05-31 17:43:37
【问题描述】:

我有一个 java 类,它可以同时被很多线程访问,并希望确保它是线程安全的。该类有一个私有字段,即字符串到字符串列表的映射。我已将 Map 实现为 ConcurrentHashMap 以确保获取和放置是线程安全的:

public class ListStore {

  private Map<String, List<String>> innerListStore;

  public ListStore() {
    innerListStore = new ConcurrentHashMap<String, List<String>>();
  }
  ...
}

因此,鉴于 Map 的获取和放置是线程安全的,我关心的是存储在 Map 中的列表。例如,考虑以下方法来检查给定条目是否存在于商店的给定列表中(为简洁起见,我省略了错误检查):

public boolean listEntryExists(String listName, String listEntry) {

  List<String> listToSearch = innerListStore.get(listName);

  for (String entryName : listToSearch) {
    if(entryName.equals(listEntry)) {
      return true;
    }
  }

  return false;
}

看来我需要同步该方法的全部内容,因为如果另一个方法在此方法迭代时更改了 innerListStore.get(listName) 处的列表内容,则会引发 ConcurrentModificationException。

正确吗?如果正确,我是在 innerListStore 上同步还是在本地 listToSearch 变量上同步?

更新:感谢您的回复。听起来我可以在列表本身上同步。更多信息,这里是 add() 方法,它可以在 listEntryExists() 方法在另一个线程中运行的同时运行:

public void add(String listName, String entryName) {

  List<String> addTo = innerListStore.get(listName);
  if (addTo == null) {
    addTo = Collections.synchronizedList(new ArrayList<String>());
    List<String> added = innerListStore.putIfAbsent(listName, addTo);
    if (added != null) {
      addTo = added;
    }
  }

  addTo.add(entryName);
}

如果这是修改存储在映射中的基础列表的唯一方法并且没有公共方法返回对映射的引用或映射中的条目,我可以在列表本身上同步迭代吗? add() 的这种实现是否足够?

【问题讨论】:

  • 您的 add() 实现已损坏。你需要正确处理putIfAbsent()的结果(否则你可能会添加到错误的列表中)。
  • @jtahlborn 你是说我需要在 putIfAbsent() 返回的 List 上调用 add() 吗?如果是这样,我不同意。 putIfAbsent() 返回与该键关联的以前的任何内容,这将是错误的列表。对吗?
  • 请重新阅读有关putIfAbsent() 方法的文档(请注意该方法的名称)。
  • 我想我现在明白你的意思并编辑了 add()。这是正确的处理方式吗?

标签: java concurrency


【解决方案1】:

您可以在 listToSearch 上进行同步(“synchronized(listToSearch) {...}”)。确保创建列表时不存在竞争条件(使用 innerListStore.putIfAbsent 来创建它们)。

【讨论】:

    【解决方案2】:

    您可以只在 listToSearch 上进行同步,没有理由在任何人只使用一个条目时锁定整个地图。

    但请记住,您需要在列表中的任何修改位置进行同步!如果您向他们传递对未同步列表的引用,同步迭代器不会自动阻止其他人执行 add() 或诸如此类的操作。

    最安全的做法是将同步列表存储在 Map 中,然后在您迭代时锁定它们,并在您返回对列表的引用时记录用户在迭代时必须对其进行同步。当没有发生实际争用时,现代 JVM 中的同步非常便宜。当然,如果您从不让对某个列表的引用从您的课程中逃脱,您可以在内部使用更精细的梳理来处理它。

    您也可以使用线程安全列表,例如使用快照迭代器的 CopyOnWriteArrayList。您需要什么样的时间点一致性是我们无法为您做出的设计决策。 javadoc 还包括对性能特征的有益讨论。

    【讨论】:

    • +1 用于提醒同步 listEntryExists 中的迭代器不会阻止其他方法中的修改。
    • 对不起,Affe,那是我,在一场反对同步的全面战争中。今天重新阅读了答案,这是完全明智的,不值得投反对票。你是对的,非竞争同步很便宜,但竞争同步不是。锁定解决方案的问题是,随着列表变大,临界区变大,争用的可能性也更大。不过,您可以通过写时复制结构的建议来解决这个问题。道歉,但 SE 不会让我现在删除反对票。
    • 没关系,我只是想问一下,因为如果我确实说错了,我想了解一下:)
    【解决方案3】:

    看来我需要同步此方法的全部内容,因为如果另一个方法在此方法对其进行迭代时更改了 innerListStore.get(listName) 处的列表内容,则会引发 ConcurrentModificationException。

    其他线程是在访问 List 本身,还是仅通过 ListStore 公开的操作?

    其他线程调用的操作会导致 Map 中存储的 a List 的内容发生变化吗?还是只会在地图中添加/删除条目?

    如果不同的线程可以导致对相同列表实例的更改,您只需要同步对存储在 Map 中的列表的访问。如果只允许线程从 Map 中添加/删除 List 实例(即更改 Map 的结构),则不需要同步。

    【讨论】:

      【解决方案4】:

      如果存储在地图中的列表属于不抛出 CME 的类型(例如CopyOnWriteArrayList),您可以随意迭代

      如果你不小心,这可能会引入一些种族

      【讨论】:

        【解决方案5】:

        如果 Map 已经是线程安全的,那么我认为同步 listToSearch 应该可以工作。我不是 100%,但我认为它应该可以工作

        synchronized(listToSearch)
        {
        
        }
        

        【讨论】:

          【解决方案6】:

          您可以使用来自Guava 的另一个抽象

          请注意,这将在整个地图上同步,因此可能对您没有那么有用。

          【讨论】:

            【解决方案7】:

            由于除了boolean listEntryExists(String listName, String listEntry) 方法之外,您没有为列表映射提供任何客户端,我想知道您为什么要存储列表?这种结构似乎更自然地是 Map&lt;String, Set&lt;String&gt;&gt; 并且 listEntryExists 应该使用 contains 方法(也可以在 List 上使用,但是对于列表的大小 O(n)):

            public boolean listEntryExists(String name, String entry) {
              SetString> set = map.get(name);
              return (set == null) ? false : set.contains(entry;
            }
            

            现在, contains 调用可以封装您想要的任何内部并发协议。

            对于add,您可以使用同步包装器(简单,但可能很慢),或者如果与读取相比写入频率较低,请使用ConcurrentMap.replace 来实现您自己的写时复制策略。比如使用番石榴ImmutableSet

            public boolean add(String name, String entry) {
              while(true) {
                SetString> set = map.get(name);
                if (set == null) {
                  if (map.putIfAbsent(name, ImmutableSet.of(entry))
                    return true
                  continue;
                }
                if (set.contains(entry)
                  return false; // no need to change, already exists
                Set<String> newSet = ImmutableSet.copyOf(Iterables.concat(set, ImmutableSet.of(entry))        
                if (map.replace(name, set, newSet)
                  return true;
              }
            }
            

            现在这是一个完全线程安全的无锁结构,并发读取器和写入器不会互相阻塞(以底层 ConcurrentMap 实现的无锁为模)。此实现在其写入中确实有一个 O(n),而您的原始实现在读取中是 O9n)。同样,如果您主要阅读而不是主要写作,这可能是一个巨大的胜利。

            【讨论】:

              猜你喜欢
              • 2013-06-28
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-01-17
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多