Java HashMap在get()调用时返回null

时间:2014-02-13 12:20:42

标签: java multithreading hashmap

我们在尝试使用HashMap中的给定键获取值时发现了NullPointerException。

以下是我将用来说明问题的示例代码。

public class Test {

    private Map<String, Integer> employeeNameToAgeMap = new HashMap<String, Integer>();

    public int getAge(String employeeName) { 
        if (!employeeNameToAgeMap.containsKey(employeeName)) {
            int age = getAgeFromSomeCustomAPI(employeeName);
            employeeNameToAgeMap.put(employeeName, age);
        }

        return employeeNameToAgeMap.get(employeeName);
    }
}

在方法的最后一行获取NullPointerException,“return employeeNameToAgeMap.get(employeeName);”

正如我们所看到的,employeeNameToAgeMap不为null,并且调用者也没有将employeeName作为null传递(这是我们已经接受了调用代码本身)。

这个方法将从不同的线程以非常快的速度调用(来自某些计划任务,计划每100毫秒运行一次)

这个NullPointerException的原因似乎是给定员工的值(年龄)为空,但事实并非如此,因为保证自定义API方法(getAgeFromSomeCustomAPI())返回某个年龄给定员工,即使它返回null,那么异常堆栈跟踪应该显示日志中的对应行而不是最后一行。

我唯一的假设是,当一个线程T1试图填充该缓存时,T2出现了,由于某种原因,它能够发现缓存已经拥有了employeeName,但是当它试图获得年龄时,它就抛出了一个NPE。但是我并不是100%确信当put()操作正在进行给定的键和值时,相同键的containsKey()返回true。

我知道需要增强此代码以解决同步问题(通过使用ConcurrentHashMap或锁定),但期待知道此问题的真正原因。

我真的很感激帮助。

4 个答案:

答案 0 :(得分:4)

  

我并非100%确信当put()操作正在进行给定的键和值时,相同键的containsKey()返回true。

你是正确的 - 没有任何同步,不能保证在一个线程中调用put()会导致另一个线程为containsKey()返回true。即使对put()的调用已经完成,也是如此。

Java内存模型允许重新排序线程的内存读/写。这通常称为无序执行。结果是任何给定线程看到的Map的内部状态可能不一致,这可能导致数据损坏,崩溃和无限循环。

对于您的简单示例,看起来您可以简单地将HashMap替换为ConcurrentHashMap,但如果没有看到您的其他程序,就很难确定这是否正确

我建议阅读Brian Goetz的 Java Concurrency in Practice ,以便更好地理解Java内存模型。

您可能也对此博文A Beautiful Race Condition感兴趣,其中显示了为什么在没有同步的线程之间共享HashMap会导致意外行为。

修改:可能值得您关注HashMap的JavaDoc的这一部分:

  

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

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

答案 1 :(得分:3)

我相信您所遇到的是在您致电

期间对HashMap的重复
return employeeNameToAgeMap.get(employeeName);

有人可以相信,如果HashMap#containsKey(key)返回true,那么应该保证,调用HashMap#get(key)也应该返回一个有效值,只要该密钥没有从HashMap中删除。这可以通过以下事实来论证:如果密钥对应于有效值,HashMap#containsKey(key)确实会检查:

public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

但这是一种致命的误解。 HashMap#containsKey(key)只能保证密钥在调用之前已经与某个值相关联。但是,如果多个线程正在访问地图,则不能保证HashMap #get(key)也会返回相应的值。造成这种差异的原因是,其他线程访问HashMap#put(key,value)与任何键值对,可能会强制重新散列HashMap,从而导致重新创建内部哈希表。如果在调用HashMap#get(key)期间发生了这样的重组,那么即使你的HashMap在调用HashMap#containsKey(key)时返回true,HashMap#get(key)也可能返回null。

如果您只想避免NullPointerException,可以这样做:

public class Test {

    private Map<String, Integer> employeeNameToAgeMap = new HashMap<String, Integer>();

    public int getAge(String employeeName) {
        final Integer age = employeeNameToAgeMap.get(employeeName);
        if (age == null) {
            age = getAgeFromSomeCustomAPI(employeeName);
            employeeNameToAgeMap.put(employeeName, age);
        }
        return (int)age;
    }
}

这当然会使您的代码线程保存,但您将不再获得您现在遇到的NullPointerException。

答案 2 :(得分:2)

这是一个很好的例子,说明为什么不被认为是线程安全的东西不应该在线程环境中使用,即使你无法想象可能出错的。这里的问题是缺乏想象力。

这里有两件事可能出错:

  • 在Java执行重新排序是你的敌人。
    如同关键字定义volatile的代码执行的顺序可以被重新排列由JVM不管出于什么目的,只要结果是在单线程环境中 。因此,可以在实际设置值之前首先添加键值对,从而导致同时get调用返回中间null值。

  • 哈希映射实现中的一些机制正在使用惰性集机制,因为在特定实现上结果更快。虽然到目前为止我看到的代码中的情况并非如此,但它告诉您,您不应该像编写代码一样期望

要学习的一课:坚持以文档和唯一的文档,因为其他一切都没有确定,因此可能会被更改或已经对你所期望太大区别

答案 3 :(得分:-1)

employeeNameToAgeMap.get(employeeName);会返回Integer。如果Integer为空,则从您的方法返回int所需的int自动取消装箱会抛出NPE。

所以你应该写一些类似的东西:

Integer result = employeeNameToAgeMap.get(employeeName);
return result == null ? -1 : result;

或者,您可以抛出异常,例如EmployeeNotFoundException

或者您也可以返回一个'Integer`文档,返回的值可能为null,并让客户端处理null case。