使用列表Map的Java并发

zmeyuzjn  于 2023-04-28  发布在  Java
关注(0)|答案(8)|浏览(205)

我有一个Java类,它同时被很多线程访问,我想确保它是线程安全的。这个类有一个私有字段,它是一个字符串到字符串列表的Map。我将Map实现为ConcurrentHashMap,以确保gets和puts是线程安全的:

public class ListStore {

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

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

因此,给定Map的get和put是线程安全的,我关心的是存储在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中的列表内容。当此方法迭代它时,将抛出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);
}

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

polkgigr

polkgigr1#

您可以在listToSearch(“synchronized(listToSearch){...}”)。确保创建列表时没有竞争条件(使用innerListStore。putIfAbsent来创建它们)。

70gysomp

70gysomp2#

你可以只在listToSearch上同步,没有理由在任何人只使用一个条目的时候锁定整个Map。
但是请记住,您需要在列表中的任何修改位置进行同步!同步迭代器不会自动阻止其他人执行add()或其他操作,如果你向他们传递了对未同步列表的引用。
最安全的做法是将同步列表存储在Map中,然后在迭代时锁定它们,并记录何时返回对列表的引用,如果用户迭代,则必须在该列表上同步。在现代JVM中,当没有实际的争用发生时,同步是相当便宜的。当然,如果您从不让对某个列表的引用脱离您的类,您可以在内部使用更精细的梳理来处理它。
或者,您可以使用线程安全列表,如使用快照迭代器的CopyOnWriteArrayList。您需要什么样的时间点一致性是我们无法为您做出的设计决策。javadoc还包括对性能特性的有益讨论。

cld4siwp

cld4siwp3#

看起来我需要同步这个方法的全部内容,因为如果另一个方法改变了innerListStore中的列表内容。当此方法迭代它时,将抛出ConcurrentModificationException。
其他线程是访问List本身,还是仅通过ListStore公开的操作?
其他线程调用的操作是否会导致存储在Map中的a List的内容被更改?还是只在Map中添加/删除条目?
如果不同的线程可能导致对相同List示例的更改,则只需要同步对存储在Map中的List的访问。如果线程仅被允许从Map添加/移除List示例(即,例如,如果用户不需要同步(例如,改变Map的结构),则同步不是必需的。

4uqofj5v

4uqofj5v4#

如果存储在Map中的列表是不抛出CME的类型(例如CopyOnWriteArrayList),则可以随意迭代
如果你不小心的话,这可能会引入一些种族

yc0p9oo0

yc0p9oo05#

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

synchronized(listToSearch)
{

}
a2mppw5e

a2mppw5e6#

你可以用Guava的另一个抽象概念
请注意,这将在整个Map上同步,因此它可能对您没有那么有用。

clj7thdc

clj7thdc7#

由于除了boolean listEntryExists(String listName, String listEntry)方法之外,您没有为列表Map提供任何客户端,我想知道您为什么要存储列表?这个结构看起来更自然地是Map<String, Set<String>>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,您可以使用同步 Package 器(简单,但可能很慢),或者如果写入与读取相比不频繁,则利用ConcurrentMap.replace实现您自己的写时复制策略。例如,使用Guava 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)。同样,如果你主要是阅读而不是写作,这可能是一个很大的胜利。

xytpbqjk

xytpbqjk8#

它可以用更少的编码来完成。下面是一个示例,它是并发压力测试Jcstress enter link description here的一部分

void add(String key, String val) {
     List<String> list = map.computeIfAbsent(key,
                    k -> Collections.synchronizedList(new ArrayList<>()));
     list.add(val);
}

相关问题