以下代码是否设置为正确同步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块,以防止在我通过调用remove()
之前另一个线程调用containsKey()
或put()
,但我做了不需要doWork()
中的同步块,因为在addToMap()
返回之前,另一个线程无法在remove()
中输入同步块,因为我最初使用Collections.synchronizedMap()
创建了Map。那是对的吗?有更好的方法吗?
答案 0 :(得分:87)
Collections.synchronizedMap()
保证您要在地图上运行的每个原子操作都会同步。
然而,必须在块中同步在地图上运行两个(或更多)操作。 所以是的 - 你正在正确同步。
答案 1 :(得分:14)
如果您使用的是JDK 6,那么您可能需要查看ConcurrentHashMap
注意该类中的putIfAbsent方法。
答案 2 :(得分:13)
您的代码中存在一个微妙的错误 potential 。
[更新:由于他正在使用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()非常适合原子插入,但不保证在该调用期间地图的其他部分不会改变;它只保证原子性。在您的示例程序中,您依赖于(同步)HashMap的同步详细信息,而不是put()s。
最后一件事。 :)来自 Java Concurrency in Practice 的这句精彩报价总能帮助我设计调试多线程程序。
对于可以由多个线程访问的每个可变状态变量,必须使用相同的锁执行对该变量的所有访问。
答案 3 :(得分:10)
是的,您正在正确同步。我将更详细地解释这一点。 只有在必须依赖于在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
)。
答案 4 :(得分: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);
答案 5 :(得分:2)
结帐Google Collections'Multimap
,例如this presentation的第28页。
如果由于某种原因无法使用该库,请考虑使用ConcurrentHashMap
代替SynchronizedHashMap
;它有一个漂亮的putIfAbsent(K,V)
方法,如果元素列表不存在,您可以使用它原子地添加元素列表。另外,如果您的使用模式需要,请考虑使用CopyOnWriteArrayList
作为地图值。
答案 6 :(得分:2)
您同步的方式是正确的。但有一个问题
但是在现实世界中,您通常会在放入值之前查询地图。因此,您需要执行两个操作,因此需要同步块。所以你使用它的方式是正确的。然而。
一个。它有一个API&#39; putIfAbsent&#39;这会做同样的事情,但效率更高。
湾它的效率:d CocurrentMap只是锁定键,因此它不会阻挡整个地图的世界。你在哪里阻止了键和值。
℃。您可能已经在代码库中的其他位置传递了地图对象的引用,您的/其他开发人员可能会错误地使用它。即,他可能只是添加()或get()而不锁定地图的对象。因此,他的通话不会与您的同步块互斥。但是使用并发实现可以让您高枕无忧 永远不会被错误地使用/实施。