多线程下的Map集合
前语
上一篇文章<>是在大考周前写的有关HashMap的文章,在其开头开头提到过ConcurrentHashMap和HashTable,因为既然讲到了Map那么就绕不开,HashMap、HashTable、ConcurrentHashMap这三兄弟,先简单介绍一下这两个新朋友:HashTable是遗留类,ConcurrentHashMap类是有点高级的,说实话在写这篇文章前,这两个类我是没用过,不是说它不重要,只能说我层次还没到。在阅读本篇文章时,我强烈建议大家先去看看<>
背景
HashMap在多线程环境下是不安全的,jdk1.7中是因为采用的是头插法,在多线程环境下两个线程同时扩容时会出现环链导致死循环;而jdk1.8中改用尾插法,避免了这个情况,但是其put操作在多线程环境下会发生覆盖,导致线程不安全。那在多线程环境下我们应该怎么做呢?
- 使用HashTable类
- 使用Collections.synchronizedMap(Map)创建线程安全的map
- 使用ConcurrentHashMap类
那么接下来我们就来细聊一下这三种方式:
HashTable
HashTable是遗留类,遗留类是指在jdk的集合框架出现前,就已经存在了,并且所有遗留类都重新设计为支持JDK5中的通用类。HashTable的很多功能都与HashMap类似,那么通过查看源代码可以发现,说HashTable是线程安全的是因为HashTable使用了synchronized关键字来保证线程同步。
我们都知道使用synchronized属于独占式的悲观锁,加上的是重量级锁,当一个线程访问HashTable的同步方法时候,其它的线程只能是阻塞或轮询状态,所以HashTable的并发性是比较差的,效率比较低。翻阅源码还可以得到以下结论:
- HashTable的默认容量为11,HashMap为16,而且HashTable不要求底层数组容量一定为2的整数次幂,HashMap则要求一定为2的整数次幂!
- HashTable没有使用红黑树,无论多大,一直是采用尾插的链表
- HashTable扩容时容量为原来的2倍+1,而HashMap为原来的二倍
- HashTable直接通过对象的hashCode()方法来获得哈希值,而HashMap中是通过HashCode方法算出的Hash值,然后与其高十六位做异或运算,得到最终的哈希值。
- HashTable不允许key和value为NULL。
为什么HashTable的key-value不能为NULL?
在源码注释中有这样一句话:
![615b0d6ac251135c92446ce0a9071fa9.png](https://i-blog.csdnimg.cn/blog_migrate/e06677731eec7910d8338c73231aca61.png)
大致意思是要从Hashtable成功存储和检索对象,用作键key的对象必须实现hashCode方法和equals方法,显然我们的NULL是不具备这两个方法的。
而value值不为空具体的表现为在其源码中有一个判空处理,为空抛出NullPointerException异常。
![4ade67f97d054503bfec845c69a13833.png](https://i-blog.csdnimg.cn/blog_migrate/6e4c8c46d0ff44f209febb479c1b67d7.png)
那么为什么要对value加上这样一个判空处理,来确保value值不为空呢? 我们现在假设允许key-value为null,那么我们使用map.get(key)时,如果返回的结果为null,那么对应的key到底是不存在,还是其value为null呢?在单线程的情况下我们当然可以通过调用map.containsKey(key)来确定key是否存在,而在多线程情况下,为了保证contains和get操作的原子性,显然这种做法在多线程的情况下我们是无法使用的。
Collections.synchronizedMap(Map)
HashTable由jdk1.1中引入,它那种直接采用Synchronized关键字来修饰方法的暴力操作,在很大程度上限制了其拓展性,因此在jdk1.2中引入了Collections.synchronizedMap(Map),这种相对较灵活的方式来保证Map的线程安全。那么让我们来看看它的源码: