Java同步块与Collections.synchronizedMap


85

是否设置了以下代码以正确同步调用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()来防止另一个线程调用,remove()或者containsKey()在我完成调用之前,put()但是我不需要同步块,doWork()因为另一个线程无法addToMap()remove()返回之前进入同步块,因为我最初创建了Map与Collections.synchronizedMap()。那是对的吗?有一个更好的方法吗?

Answers:


90

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

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


26
我认为这很有效,因为javadocs明确声明了syncedMap是在地图本身上同步的,而不是内部锁。如果是这样的话,synchronized(synchronizedMap)将是不正确的。
Extraneon 2010年

2
@Yuval您能否进一步解释您的答案?您说sychronizedMap原子地执行操作,但是如果syncMap使所有操作原子化,那么为什么还需要自己的同步块?您的第一段似乎可以避免担心第二段。
almel 2015年

@almel看到我的回答
Sergey

2
为什么必须要有同步块,因为地图已经使用了 Collections.synchronizedMap()?我没有得到第二点。
Bimal Sharma


13

还有就是潜在的代码中的一个微妙的错误。

[更新: 由于他使用的是map.remove(),因此此说明并不完全有效。我第一次错过这个事实。:(感谢问题的作者指出这一点。我将其余内容保留原样,但是将Lead语句更改为说可能存在错误。]

doWork()中,您以线程安全的方式从Map中获取List值。但是,此后,您出于安全考虑访问该列表。例如,一个线程可能正在使用doWork()中的列表,而另一个线程可能addToMap()中调用syncedMap.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。这是一个非常有用的类,但并不总是适合替代同步的HashMaps。引用其Javadocs,

在依赖于其线程安全性但不依赖于其同步详细信息的程序中,此类可以与Hashtable完全互操作。

换句话说,putIfAbsent()非常适合于原子插入,但不能保证映射的其他部分在该调用期间不会改变。它仅保证原子性。在示例程序中,您依赖(同步的)HashMap的同步详细信息来处理put()之外的事情。

最后一件事。:) Java Concurrency in Practice中的这一出色报价始终可以帮助我设计调试多线程程序。

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


如果我使用syncedMap.get()访问列表,那么我会看到您对该错误的看法。由于我正在使用remove(),因此下一次使用该键添加的对象不应该创建一个新的ArrayList并且不干扰我在doWork中使用的那个吗?
Ryan Ahearn

正确!我微风轻拂了你的删除。
JLR,2009年

1
对于可能由多个线程访问的每个可变状态变量,必须使用相同的锁来执行对该变量的所有访问。----我通常会添加一个私有属性,那就是一个新的Object()并将其用于我的同步块。这样,我就可以从上下文中了解所有内容。同步(objectInVar){}
AnthonyJClink 2014年

11

是的,您正在正确同步。我将更详细地解释这一点。仅在必须在syncedMap对象上的方法调用序列中依赖于先前方法调用的结果的情况下,才必须同步syncedMap对象上的两个或多个方法调用。让我们看一下这段代码:

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)。


4

在我看来,这是正确的。如果要进行任何更改,我将停止使用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);

3
该做的事。我不明白为什么Collections.synchronizedXXX()在我们每天的应用程序逻辑中仍然必须在某个对象(大多数情况下只是集合本身)上进行同步时为什么应该使用API
kellogs 2015年

3

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

  1. Collection框架提供的同步包装程序可确保方法调用(即add / get / contains)将互斥运行。

但是,在现实世界中,通常需要先查询地图,然后再输入值。因此,您将需要执行两个操作,因此需要一个同步块。因此,您使用它的方式是正确的。然而。

  1. 您可能已经使用了Collection框架中可用的Map的并发实现。“ ConcurrentHashMap”的好处是

一种。它有一个API'putIfAbsent',可以做同样的事情,但是效率更高。

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

C。您可能已经在代码库中的其他地方传递了地图对象的引用,您/您的Tean中的其他开发人员最终可能会错误地使用它。也就是说,他可能只是全部add()或get()而不锁定地图对象。因此,他的通话不会与您的同步块互斥。但是,使用并发实现可以让您放心,它永远不会被错误地使用/实现。


2

看看谷歌集合Multimap,如第28页此演示文稿

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.