上一篇 List 踩坑文章中,咱们提到几个比较容易踩坑的点。做为 List 集合好兄弟 Map,咱们也是每天都在使用,一不当心也会踩坑。html
今天我就来总结这些常见的坑,再捞本身一手,防止后续同窗再继续踩坑。安全
本文设计知识点以下:多线程
这个踩坑经历仍是发生在实习的时候,那时候有这样一段业务代码,功能很简单,从 XML 中读取相关配置,存入 Map 中。并发
代码示例以下:ide
那时候正好有个小需求,须要改动一下这段业务代码。改动的过程当中,忽然想到 HashMap
并发过程可能致使死锁的问题。性能
因而改动了一下这段代码,将 HashMap
修改为了 ConcurrentHashMap
。线程
美滋滋提交了代码,而后当天上线的时候,就发现炸了。。。设计
应用启动过程发生 NPE 问题,致使应用启动失败。3d
根据异常日志,很快就定位到了问题缘由。因为 XML 某一项配置问题,致使读取元素为 null,而后元素置入到 ConcurrentHashMap
中,抛出了空指针异常。指针
这不科学啊!以前 HashMap
都没问题,均可以存在 null,为何它老弟 ConcurrentHashMap
就不能够?
翻阅了一下 ConcurrentHashMap#put
方法的源码,开头就看到了对 KV 的判空校验。
看到这里,不知道你有没有疑惑,为何 ConcurrentHashMap
与 HashMap
设计的判断逻辑不同?
求助了下万能的 Google,找到 Doug Lea 老爷子的回答:
来源:http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002485.html
总结一下:
上面提到 Josh Bloch 正是 HashMap
做者,他与 Doug Lea 在 null 问题意见并不一致。
也许正是由于这些缘由,从而致使 ConcurrentHashMap
与 HashMap
对于 null 处理并不同。
最后贴一下经常使用 Map 子类集合对于 null 存储状况:
上面的实现类约束,都太不同,有点很差记忆。其实只要咱们在加入元素以前,主动去作空指针判断,不要在 Map 中存入 null,就能够从容避免上面问题。
先来看个简单的例子,咱们自定义一个 Goods
商品类,将其做为 Key 存在 Map 中。
示例代码以下:
上面代码中,第二次咱们加入一个相同的商品,本来咱们指望新加入的值将会替换原来旧值。可是实际上这里并无替换成功,反而又加入一对键值。
翻看一下 HashMap#put
的源码:
如下代码基于 JDK1.7
这里首先判断 hashCode
计算产生的 hash,若是相等,再判断 equals
的结果。可是因为 Goods
对象未重写的hashCode
与 equals
方法,默认状况下 hashCode
将会使用父类对象 Object 方法逻辑。
而 Object#hashCode
是一个 native 方法,默认将会为每个对象生成不一样 hashcode(与内存地址有关),这就致使上面的状况。
因此若是须要使用自定义对象作为 Map 集合的 key,那么必定记得重写hashCode
与 equals
方法。
而后当你为自定义对象重写上面两个方法,接下去又可能踩坑另一个坑。
使用 lombok 的
EqualsAndHashCode
自动重写hashCode
与equals
方法。
上面的代码中,当 Map 中置入自定义对象后,接着修改了商品金额。而后当咱们想根据同一个对象取出 Map 中存的值时,却发现取不出来了。
上面的问题主要是由于 get
方法是根据对象 的 hashcode 计算产生的 hash 值取定位内部存储位置。
当咱们修改了金额字段后,致使 Goods
对象 hashcode 产生的了变化,从而致使 get 方法没法获取到值。
经过上面两种状况,能够看到使用自定义对象做为 Map 集合 key,仍是挺容易踩坑的。
因此尽可能避免使用自定义对象做为 Map 集合 key,若是必定要使用,记得重写 hashCode
与 equals
方法。另外还要保证这是一个不可变对象,即对象建立以后,没法再修改里面字段值。
以前的文章『天天都在用 Map,这些核心技术你知道吗?』咱们说过 HashMap
是一个线程不安全的容器,多线程环境为了线程安全,咱们须要使用 ConcurrentHashMap
代替。
可是不要认为使用了 ConcurrentHashMap
必定就能保证线程安全,在某些错误的使用场景下,依然会形成线程不安全。
上面示例代码,咱们本来指望输出 1001,可是运行几回,获得结果都是小于 1001。
深刻分析这个问题缘由,其实是由于第一步与第二步是一个组合逻辑,不是一个原子操做。
ConcurrentHashMap
只能保证这两步单的操做是个原子操做,线程安全。可是并不能保证两个组合逻辑线程安全,颇有可能 A 线程刚经过 get 方法取到值,还将来得及加 1,线程发生了切换,B 线程也进来取到一样的值。
这个问题一样也发生在其余线程安全的容器,好比 Vector
等。
上面的问题解决办法也很简单,加锁就能够解决,不过这样就会使性能大打折扣,因此不太推荐。
咱们可使用 AtomicInteger
解决以上的问题。
上一篇文章中咱们提过,Arrays#asList
与 List#subList
返回 List 将会与原集合互相影响,且可能并不支持 add
等方法。一样的,这些坑爹的特性在 Map 中也存在,一不当心,将会再次掉坑。
Map 接口除了支持增删改查功能之外,还有三个特有的方法,能返回全部 key,返回全部的 value,返回全部 kv 键值对。
// 返回 key 的 set 视图 Set<K> keySet(); // 返回全部 value Collection 视图 Collection<V> values(); // 返回 key-value 的 set 视图 Set<Map.Entry<K, V>> entrySet();
这三个方法建立返回新集合,底层其实都依赖的原有 Map 中数据,因此一旦 Map 中元素变更,就会同步影响返回的集合。
另外这三个方法返回新集合,是不支持的新增以及修改操做的,可是却支持 clear、remove
等操做。
示例代码以下:
因此若是须要对外返回 Map 这三个方法产生的集合,建议再来个套娃。
new ArrayList<>(map.values());
最后再简单提一下,使用 foreach
方式遍历新增/删除 Map 中元素,也将会和 List 集合同样,抛出 ConcurrentModificationException
。
从上面文章能够看到不论是 List 提供的方法返回集合,仍是 Map 中方法返回集合,底层实际仍是使用原有集合的元素,这就致使二者将会被互相影响。因此若是须要对外返回,请使用套娃大法,这样让别人用的也安心。
第二, Map 各个实现类对于 null 的约束都不太同样,这里建议在 Map 中加入元素以前,主动进行空指针判断,提早发现问题。
第三,慎用自定义对象做为 Map 中的 key,若是须要使用,必定要重写 hashCode
与 equals
方法,而且还要保证这是个不可变对象。
第三,ConcurrentHashMap
是线程安全的容器,可是不要思惟定势,不要片面认为使用 ConcurrentHashMap
就会线程安全。