线程问题在Java HashMap中

时间:2008-10-28 18:31:20

标签: java multithreading collections concurrency hashmap

发生了一些我不确定应该可行的事情。显然它是,因为我已经看过了,但我需要找到根本原因&我希望你们都能帮忙。

我们有一个查找纬度和频率的系统经度为邮政编码。我们不是每次都访问它,而是将结果缓存在便宜的内存中HashTable缓存中,因为lat&很长一段邮政编码的变化往往比我们发布的更少。

无论如何,哈希被一个具有“get”和“add”方法的类所包围,这两个方法都是同步的。我们以单身形式访问此类。

我并不是说这是最好的设置,但它就是我们所处的位置。 (我计划更改为尽快调用Collections.synchronizedMap()中的Map。)

我们在多线程环境中使用此缓存,其中我们为2个拉链进行2次调用(因此我们可以计算两者之间的距离)。这些有时几乎同时发生,因此两个调用很可能同时访问地图。

就在最近,我们遇到了两个不同邮政编码返回相同值的事件。假设初始值实际上是不同的,有没有办法将值写入Map会导致为两个不同的键写入相同的值?或者,2“获取”是否有任何方式可以穿过电线并意外地返回相同的值?

我唯一的另一个解释是初始数据已损坏(错误的值),但似乎不太可能。

任何想法都将不胜感激。 谢谢, 彼得

(PS:如果您需要更多信息,代码等,请告诉我。)

public class InMemoryGeocodingCache implements GeocodingCache
{

private Map cache = new HashMap();
private static GeocodingCache instance = new InMemoryGeocodingCache();

public static GeocodingCache getInstance()
{
    return instance;
}

public synchronized LatLongPair get(String zip)
{
    return (LatLongPair) cache.get(zip);
}

public synchronized boolean has(String zip)
{
    return cache.containsKey(zip);
}

public synchronized void add(String zip, double lat, double lon)
{
    cache.put(zip, new LatLongPair(lat, lon));
}
}


public class LatLongPair {
double lat;
double lon;

LatLongPair(double lat, double lon)
{
    this.lat = lat;
    this.lon = lon;
}

public double getLatitude()
{
    return this.lat;
}

public double getLongitude()
{
    return this.lon;
}
}

8 个答案:

答案 0 :(得分:8)

代码看起来正确。

唯一的问题是lat和lon是包可见的,因此对于相同的包代码可以使用以下内容:

LatLongPair llp = InMemoryGeocodingCache.getInstance().get(ZIP1);
llp.lat = x;
llp.lon = y;

显然会修改缓存中的对象。

所以也要做lat和lon final。

P.S。由于您的密钥(zip-code)是唯一且小的,因此无需在每个操作上计算哈希值。使用TreeMap(包装到Collections.synchronizedMap())更容易。

P.P.S。实用方法:为两个线程编写测试,在永不停止的循环中执行put / get操作,在每次获取时验证结果。你需要一台多CPU机器。

答案 1 :(得分:6)

为什么发生这种情况很难说清楚。更多代码可以提供帮助。

你应该只是使用ConcurrentHashMap。一般来说,这比同步Map更有效。您不同步对它的访问,它在内部处理它(比您更有效)。

答案 2 :(得分:4)

要注意的一件事是,如果键或值可能正在改变,例如,如果不是为每个插入创建一个新对象,那么您只需更改现有对象的值并重新插入即可。

您还希望确保密钥对象以不违反HashMap契约的方式定义hashCode和equals(即如果equals返回true,则hashCodes需要相同,但不一定是恶意亦然)。

答案 3 :(得分:3)

是否有可能修改LatLonPair?我建议将lat和lon字段设为final,这样它们就不会在代码中的其他地方被意外修改。

请注意,您还应该使您的单例“实例”和地图引用“缓存”最终。

答案 4 :(得分:2)

詹姆斯是对的。由于您正在交还一个对象,因此可以修改其内部结构,并且任何持有对该对象(地图)的引用的内容都将反映该更改。决赛是一个很好的答案。

答案 5 :(得分:0)

我发现您发布的代码没有任何问题会导致您所描述的问题。我的猜测是,你的地理代码缓存客户端存在问题。

需要考虑的其他事项(其中一些非常明显,但我认为无论如何我都会指出它们):

  1. 您遇到哪两个邮政编码?您确定它们在源系统中没有相同的地理编码吗?
  2. 您确定不会意外地比较两个相同的邮政编码吗?

答案 6 :(得分:0)

有(String ZIP)方法的存在意味着您的代码中包含以下内容:

GeocodingCache cache = InMemoryGeocodingCache.getInstance();

if (!cache.has(ZIP)) {
    cache.add(ZIP, x, y);
}

不幸的是,这会让你开始同步has()返回false和add()添加之间的问题,可以导致你所描述的问题。

更好的解决方案是在add方法中移动检查,以便检查和更新由同一个锁覆盖,如:

public synchronized void add(String zip, double lat, double lon) {
    if (cache.containsKey(zip)) return;
    cache.put(zip, new LatLongPair(lat, lon));
}

我应该提到的另一件事是,如果你使用 getInstance()作为单例,你应该有一个私有构造函数来阻止使用 new InMemoryGeocodingCache创建额外的缓存( )

答案 7 :(得分:0)

这是关于HashMap的java文档:

http://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html

请注意,此实现未同步。如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。)这通常通过同步自然封装映射的某个对象来完成。 。如果不存在此类对象,则应使用Collections.synchronizedMap方法“包装”该映射。这最好在创建时完成,以防止意外地不同步访问地图:

Map m = Collections.synchronizedMap(new HashMap(...));

或者更好,使用java.util.concurrent.ConcurrentHashMap