【问题标题】:Threading issues in a Java HashMapJava HashMap 中的线程问题
【发布时间】:2008-10-28 18:31:19
【问题描述】:

发生了一些我不确定是否可能发生的事情。显然是这样,因为我已经看到了,但我需要找到根本原因,希望大家能提供帮助。

我们有一个系统可以查找邮政编码的纬度和经度。我们不是每次都访问它,而是将结果缓存在一个廉价的内存中 HashTable 缓存中,因为邮政编码的纬度和经度往往比我们发布的变化少。

无论如何,散列被一个类包围,该类具有同步的“get”和“add”方法。我们作为单例访问这个类。

我并不是说这是最好的设置,但它就是我们所处的位置。 (我计划尽快将地图包装在 Collections.synchronizedMap() 调用中。)

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

就在最近我们发生了一个事件,两个不同的邮政编码返回相同的值。假设初始值实际上不同,有没有办法将值写入 Map 会导致为两个不同的键写入相同的值?或者,有什么方法可以让 2 个“gets”跨线并意外返回相同的值?

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

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

(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;
}
}

【问题讨论】:

  • 我没有看到任何使用“实例”的东西。它有什么用?此外,如果你制作了缓存“Map”,你在做什么会更清楚。
  • 我会非常仔细地检查调用 InMemoryGeocodingCache.add 的所有内容。
  • 如果 LatLongPair 是真正不可变的(没有设置器),您应该将 lat 和 lon 设为最终值。从安全发布/java 内存模型的并发角度来看,这是有意义的。

标签: java multithreading collections concurrency hashmap


【解决方案1】:

代码看起来正确。

唯一需要注意的是 lat 和 lon 是包可见的,因此对于相同的包代码,以下是可能的:

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

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

所以也将 lat 和 lon 设为 final。

附:由于您的密钥(邮政编码)是唯一且小的,因此无需在每个操作上计算哈希。使用 TreeMap 更容易(封装在 Collections.synchronizedMap() 中)。

附言实用方法:为两个线程在永无止境的循环中执行 put/get 操作编写测试,在每次 get 时验证结果。不过,您需要一台多 C​​PU 机器。

【讨论】:

  • 为什么要将它们设为私有?这些值旨在是不可变的。编写一个 get() 方法只是为了使成员变量私有是一个偏好问题,我想 - 但也许考虑到 getter 并不总是正确的事情是可以的?
  • 你说得对,Kevin,private 不是教条,如果字段是最终字段,则在这种情况下不需要。这只是一种常见的做法。
  • 我同意这些字段应该是最终的。
【解决方案2】:

为什么会这样很难说。更多代码可能会有所帮助。

无论如何,您可能应该只使用 ConcurrentHashMap。一般来说,这将比同步 Map 更有效。您不同步对它的访问,它在内部处理它(比您更有效)。

【讨论】:

    【解决方案3】:

    需要注意的一点是键或值是否可能发生变化,例如,如果不是为每次插入创建一个新对象,您只是更改现有对象的值并重新插入它。

    您还需要确保 key 对象同时定义 hashCode 和 equals 以不违反 HashMap 协定(即如果 equals 返回 true,则 hashCodes 必须相同,但不一定相反反之亦然)。

    【讨论】:

    • 他使用String作为key——不用担心哈希码
    • 这原本并不明显,因为我在发表评论后添加了代码。
    【解决方案4】:

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

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

    【讨论】:

    • 我看不出 LatLong 是如何被修改的。您可以看到它已创建并已放入地图中。我应该仔细检查以确保我们在稍后将其取出后没有对其做任何有趣的事情......
    • 好的,在使用缓存的代码中,我们提取返回的 LatLongPair 并使用 lat/long 值创建一个新对象。因此,它的使用非常有限,并且值仅写入构造函数中。但值得一看。
    【解决方案5】:

    詹姆斯是对的。由于您要交回一个对象,因此可以修改其内部结构,并且任何持有对该对象(映射)的引用的东西都将反映该更改。 Final 是一个很好的答案。

    【讨论】:

      【解决方案6】:

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

      其他需要考虑的事情(其中一些很明显,但我想我还是会指出它们):

      1. 您遇到了哪两个邮政编码问题?您确定它们在源系统中没有相同的地理编码吗?
      2. 您确定不是不小心比较了两个相同的邮政编码吗?

      【讨论】:

        【解决方案7】:

        has(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( )

        【讨论】:

          【解决方案8】:

          这是关于 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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2014-03-06
            • 2012-04-12
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多