2011-05-31 31 views
3

我有一个一次可以被很多线程访问的java类,并且要确保它是线程安全的。该类有一个私有字段,它是一个字符串列表的字符串映射。我已经实现了地图作为一个ConcurrentHashMap以确保获得期权和看跌期权是线程安全的:使用列表映射的Java并发性

public class ListStore { 

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

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

所以考虑到获取并把该地图是线程安全的,我关心的是存储在地图列表。例如,考虑下面的方法来检查,如果在给定的列表中存在在店里给定的条目(我省略了错误检查简洁):

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()的实现是否足够?

+0

你的add()实现被破坏了。你需要正确处理'putIfAbsent()'的结果(否则你可能会添加到错误的列表中)。 – jtahlborn 2011-05-31 18:55:22

+0

@jtahlborn你是说我需要在putIfAbsent()返回的List上调用add()吗?如果是这样,我不同意。 putIfAbsent()返回之前与该键关联的**,这将是错误的列表。对? – Cameron 2011-05-31 19:00:37

+1

请重新阅读'putIfAbsent()'方法的文档(注意该方法的名称)。 – jtahlborn 2011-05-31 19:12:32

回答

1

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

只要记住,你需要在列表中的任何地方进行同步修改!同步迭代器不会自动阻止其他人执行add(),或者如果向他们传递对非同步列表的引用,则不会自动阻止其他人执行add()。

将同步列表存储在Map中,然后在迭代时锁定它们,并且在返回列表引用时记录用户必须在其迭代时必须同步的参考文档时最安全。当没有实际的争用发生时,现代JVM中的同步是相当便宜的。当然,如果你从不让一个参考列表逃离班级,你可以用一个更精细的梳子在内部处理它。

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

+0

+1时才起作用,提醒同步'listEntryExists'中的迭代器不会阻止其他方法中的修改。 – CPerkins 2011-05-31 18:29:01

+0

随机downvote for? – Affe 2011-06-01 17:27:49

+0

对不起,这就是我,一场反对同步的全面战争。今天重新阅读答案是非常明智的,不值得投票。你是对的,无争议的同步是便宜的,但争夺同步不是。锁定解决方案的问题在于,随着列表变大,临界区域变大,争用的可能性增加。你尽管用写时复制结构的建议来解决这个问题。道歉,但SE不会让我现在删除倒票。 – 2011-06-02 23:18:37

0

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

synchronized(listToSearch) 
{ 

} 
+0

只有当列表已经是同步列表 – Affe 2011-05-31 17:53:25

2

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

0

如果存储在地图中的列表是不扔CME(CopyOnWriteArrayList为例)的类型,你可以随意

但如果你不小心,这会带来一些比赛迭代

1

这似乎是我需要这个方法的全部内容同步,因为如果一种方法在innerListStore.get(LISTNAME)改变列表的内容,但这种方法上进行迭代,一个ConcurrentModificationException的会被抛出。

其他线程是否访问列表本身,或只有通过ListStore公开的操作?

其他线程调用的操作是否会导致存储在Map中的List的内容被更改?或者只会将条目添加到地图中或从中删除?

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

+0

请参阅我的更新的答案。 – Cameron 2011-05-31 18:41:42

0

你可以使用另一个抽象从Guava

注意,这将在整个地图上同步,所以它可能是对你没有多大用处的。

0

由于除了boolean listEntryExists(String listName, String listEntry)方法之外,您还没有为列表映射提供任何客户端,所以我想知道为什么您要存储列表?这种结构似乎更加自然一个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; 
} 

现在,包含通话能封装你想要的任何内部并发协议。

对于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)。再次,如果你是阅读 - 主要而不是写 - 主要是这可能是一个巨大的胜利。