繁体   English   中英

Java synchronized块与Collections.synchronizedMap

[英]Java synchronized block vs. Collections.synchronizedMap

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

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

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

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

请注意该类中的putIfAbsent方法。

您的代码中可能存在细微的错误。

[ 更新:因为他正在使用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的快速说明。 这是一个非常有用的类,但并不总是适用于同步HashMaps的替代品。 引用其Javadocs,

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

换句话说,putIfAbsent()非常适合原子插入,但不保证在该调用期间地图的其他部分不会改变; 它只保证原子性。 在您的示例程序中,您依赖于put()s以外的(同步)HashMap的同步详细信息。

最后一件事。 :)来自Java Concurrency in Practice的这句精彩报价总能帮助我设计调试多线程程序。

对于可由多个线程访问的每个可变状态变量,必须在保持相同锁的情况下执行对该变量的所有访问。

是的,您正在正确同步。 我将更详细地解释这一点。 只有在必须依赖于在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对象包含key并且将抛出NullPointerException因为synchronizedMap.get(key)将返回null 如果synchronizedMap对象上的方法调用序列不依赖于彼此的结果,那么您不需要同步序列。 例如,您不需要同步此序列:

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

这里

synchronizedMap.put(key2, valuesList2);

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

synchronizedMap.put(key1, valuesList1);

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

这对我来说是正确的。 如果我要改变任何东西,我会停止使用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);

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

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

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

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

一个。 它有一个API'putIfAbsent',它会以更有效的方式执行相同的操作。

它的效率:d CocurrentMap只是锁定键,因此它不会阻挡整个地图的世界。 你在哪里阻止了键和值。

C。 您可能已经在代码库中的其他位置传递了地图对象的引用,您的/其他开发人员可能会错误地使用它。 即他可能只是添加()或get()而不锁定地图的对象。 因此,他的调用不会与您的同步块互斥。 但是使用并发实现可以让您高枕无忧,它永远不会被错误地使用/实现。

查看Google CollectionsMultimap ,例如本演示文稿的第28页。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM