Java 同步块与 Collections.synchronizedMap

Posted

技术标签:

【中文标题】Java 同步块与 Collections.synchronizedMap【英文标题】:Java synchronized block vs. Collections.synchronizedMap 【发布时间】:2010-10-08 16:47:35 【问题描述】:

以下代码是否设置为正确同步 synchronizedMap 上的调用?

public class MyClass 
  private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());

  public void doWork(String key) 
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) 
      //do something with values
    
  

  public static void addToMap(String key, String value) 
    synchronized (synchronizedMap) 
      if (synchronizedMap.containsKey(key)) 
        synchronizedMap.get(key).add(value);
      
      else 
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
      
    
  

据我了解,我需要 addToMap() 中的同步块来防止另一个线程在调用 put() 之前调用 remove()containsKey(),但我不需要 @987654327 中的同步块@ 因为在 remove() 返回之前另一个线程无法进入 addToMap() 中的同步块,因为我最初使用 Collections.synchronizedMap() 创建了 Map。那是对的吗?有没有更好的方法来做到这一点?

【问题讨论】:

【参考方案1】:

Collections.synchronizedMap() 保证您要在地图上运行的每个原子操作都会同步。

但是,在地图上运行两个(或更多)操作必须在一个块中同步。 所以是的 - 你正在正确同步。

【讨论】:

我认为最好提一下这可行,因为 javadocs 明确声明 synchronizedMap 在地图本身上同步,而不是一些内部锁。如果是这样,synchronized(synchronizedMap) 就不正确了。 @Yuval 你能更深入地解释你的答案吗?您说 sychronizedMap 以原子方式执行操作,但是如果 syncMap 使您的所有操作都成为原子操作,您为什么还需要自己的同步块呢?您的第一段似乎排除了对第二段的担忧。 @almel 看我的answer 为什么需要同步块,因为地图已经使用Collections.synchronizedMap()?我没有得到第二点。【参考方案2】:

如果您使用的是 JDK 6,那么您可能需要查看 ConcurrentHashMap

注意该类中的 putIfAbsent 方法。

【讨论】:

【参考方案3】:

潜在在您的代码中出现一个细微的错误。

[更新: 因为他使用的是 map.remove(),所以这个描述并不完全有效。我第一次错过了这个事实。 :( 感谢问题的作者指出这一点。我将保留其余部分,但将主要声明更改为可能存在错误。]

doWork() 中,您以线程安全的方式从 Map 中获取 List 值。但是,之后,您在不安全的情况下访问该列表。例如,一个线程可能正在使用 doWork() 中的列表,而另一个线程在 addToMap() 中调用 synchronizedMap.get(key).add(value) 。这两个访问不同步。经验法则是集合的线程安全保证不会扩展到它们存储的键或值。

您可以通过将同步列表插入到地图中来解决此问题

List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list

或者,您可以在访问 doWork() 中的列表时在地图上进行同步:

  public void doWork(String key) 
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) 
      synchronized (synchronizedMap) 
          //do something with values
      
    
  

最后一个选项会稍微限制并发性,但在 IMO 上更清晰一些。

另外,关于 ConcurrentHashMap 的简要说明。这是一个非常有用的类,但并不总是同步 HashMap 的合适替代品。引用其 Javadocs,

在依赖线程安全但不依赖同步细节的程序中,此类与 Hashtable 完全可互操作

换句话说,putIfAbsent() 非常适合原子插入,但不能保证映射的其他部分在调用期间不会改变;它只保证原子性。在您的示例程序中,除了 put()s 之外,您还依赖(同步的)HashMap 的同步细节。

最后一件事。 :) Java Concurrency in Practice 中的这句话总是帮助我设计调试多线程程序。

对于每个可能被多个线程访问的可变状态变量,对该变量的所有访问都必须使用相同的锁来执行。

【讨论】:

如果我使用synchronizedMap.get() 访问列表,我明白您关于该错误的观点。由于我使用的是 remove(),下一次添加该键是否应该创建一个新的 ArrayList 并且不干扰我在 doWork 中使用的那个? 正确!我完全摆脱了你的删除。 对于每个可能被多个线程访问的可变状态变量,对该变量的所有访问都必须使用相同的锁来执行。 ---- 我通常添加一个私有属性,它只是一个新的 Object() 并将其用于我的同步块。这样我就可以通过原始的上下文了解它的全部内容。同步 (objectInVar)【参考方案4】:

是的,您正在正确同步。我将更详细地解释这一点。 只有在对 synchronizedMap 对象的方法调用序列中的后续方法调用中必须依赖先前方法调用的结果时,您才必须在 synchronizedMap 对象上同步两个或多个方法调用。 我们来看看这段代码:

synchronized (synchronizedMap) 
    if (synchronizedMap.containsKey(key)) 
        synchronizedMap.get(key).add(value);
    
    else 
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
    

在这段代码中

synchronizedMap.get(key).add(value);

synchronizedMap.put(key, valuesList);

方法调用依赖于之前的结果

synchronizedMap.containsKey(key)

方法调用。

如果方法调用的顺序没有同步,结果可能是错误的。 例如thread 1 正在执行addToMap() 方法,thread 2 正在执行doWork() 方法 synchronizedMap 对象上的方法调用顺序可能如下: Thread 1已经执行了方法

synchronizedMap.containsKey(key)

结果是“true”。 在该操作系统将执行控制切换到thread 2 并执行后

synchronizedMap.remove(key)

执行控制切换回thread 1之后,例如已经执行了

synchronizedMap.get(key).add(value);

相信synchronizedMap 对象包含keyNullPointerException 将被抛出,因为synchronizedMap.get(key) 将返回null。 如果synchronizedMap 对象上的方法调用序列不依赖于彼此的结果,则不需要同步序列。 例如你不需要同步这个序列:

synchronizedMap.put(key1, valuesList1);
synchronizedMap.put(key2, valuesList2);

这里

synchronizedMap.put(key2, valuesList2);

方法调用不依赖于前面的结果

synchronizedMap.put(key1, valuesList1);

方法调用(它不关心在两个方法调用之间是否有线程干扰,例如删除了key1)。

【讨论】:

【参考方案5】:

这对我来说是正确的。如果我要更改任何内容,我将停止使用 Collections.synchronizedMap() 并以相同的方式同步所有内容,以使其更清晰。

另外,我会替换

  if (synchronizedMap.containsKey(key)) 
    synchronizedMap.get(key).add(value);
  
  else 
    List<String> valuesList = new ArrayList<String>();
    valuesList.add(value);
    synchronizedMap.put(key, valuesList);
  

List<String> valuesList = synchronziedMap.get(key);
if (valuesList == null)

  valuesList = new ArrayList<String>();
  synchronziedMap.put(key, valuesList);

valuesList.add(value);

【讨论】:

要做的事情。我不明白为什么我们仍然需要在我们日常应用程序的逻辑中对某个对象(在大多数情况下只是集合本身)进行同步时,我们应该使用Collections.synchronizedXXX() API【参考方案6】:

您同步的方式是正确的。但是有一个问题

    Collection 框架提供的同步包装器确保方法调用,即 add/get/contains 将互斥运行。

但在现实世界中,您通常会在输入值之前查询地图。因此,您需要执行两个操作,因此需要一个同步块。所以你使用它的方式是正确的。但是。

    您可以使用 Collection 框架中可用的 Map 的并发实现。 'ConcurrentHashMap' 的好处是

一个。它有一个 API 'putIfAbsent',它可以做同样的事情,但以更有效的方式。

b.它的高效: dThe CocurrentMap 只是锁定键,因此它不会阻塞整个地图的世界。您在哪里阻止了键和值。

c。您可能已经在代码库中的其他地方传递了您的地图对象的引用,您/Tean 中的其他开发人员最终可能会错误地使用它。即他可能只是所有 add() 或 get() 而不锁定地图的对象。因此,他的呼叫不会与您的同步块相互排斥。但是使用并发实现可以让您高枕无忧 永远不能错误地使用/实现。

【讨论】:

【参考方案7】:

查看Google Collections'Multimap,例如this presentation 第 28 页。

如果由于某种原因您不能使用该库,请考虑使用ConcurrentHashMap 而不是SynchronizedHashMap;它有一个漂亮的putIfAbsent(K,V) 方法,如果元素列表不存在,您可以使用它自动添加元素列表。此外,如果您的使用模式允许,请考虑使用 CopyOnWriteArrayList 作为地图值。

【讨论】:

以上是关于Java 同步块与 Collections.synchronizedMap的主要内容,如果未能解决你的问题,请参考以下文章

java源码之HashSet

多线程之同步代码块与同步函数

java.util.TreeMap源码分析

从代码层读懂HashMap的实现原理

Java基础6:代码块与代码加载顺序

Java基础6:代码块与代码加载顺序