一.Map接口介绍
![]() | ![]() |
Map接口的继承树如上图。
我们经常使用到的是HashMap和Hashtable。
Hashtable:线程安全的,不允许null的key和value。多线程中不使用hashtable,因为HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。当一个线程和其他线程同时访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
HashMap:线程不安全的,允许空key和value。
LinkedHashMap:链表方式存储HashMap,可以记住前后顺序,但查询较慢。
TreeMap:将存储的键值对进行默认排序,且能够指定排序的比较器;线程不安全的;不允许键值为null。
二.HashMap原理
术语 | 英文 | 解释 |
哈希算法 | hash algorithm | 是一种将任意内容的输入转换成相同长度输出的加密方式,其输出被称为哈希值。 |
哈希表 | hash table | 根据设定的哈希函数H(key)和处理冲突方法将一组关键字映象到一个有限的地址区间上,并以关键字在地址区间中的象作为记录在表中的存储位置,这种表称为哈希表或散列,所得存储位置称为哈希地址或散列地址。 |
哈希map使用的是哈希表,哈希表有多种不同的实现方法,常见的有:数组+链表实现方式,即:链表的数组。
首先,HashMap中存储的对象为数组Entry[](每个Entry就是一个<key,value>),存取时根据key的hashCode相关的算法得到数组的下标,put和get都根据算法的下标取得元素在数组中的位置。当key有相同的下标时,使用链表,把下标相同的 Entry 放到一个链表中。存储时存到第一个位置,并把next指向之前的第一个元素(如果是已存在的key,则需要遍历链表);取值时遍历链表,通过equals方法获取Entry。
// 存储时:
int hash = key.hashCode(); // 每个key的hash是一个固定的int值
int index = hash & (Entry[].length-1); // 二进制的按位与运算
Entry[index] = value;
// 取值时:
int hash = key.hashCode();
int index = hash & (Entry[].length-1); // 二进制的按位与运算
return Entry[index]; // 此处需要遍历 Entry[index] 的链表取值,此处为简写
关于原理细节,可参考文章 https://www.cnblogs.com/holyshengjie/p/6500463.html 前半部分
关于源码实现,可参考文章 https://www.cnblogs.com/chengxiao/p/6059914.html
由此可见,当hashcode值设计不行,若返回的key是同一个下标,那么效率也会低。同时当哈希值冲突且大于阀值的size时,会以2次幂进行增长,因为获取数组下标的方法是【hashCode和数组容量的按位与运算】结果,故只有数组大小为2^n-1(即二进制所有位都是1,如:3,7,15,31...)才能保按位与运算得到的 index 均匀分布。
三.ConcurrentHashMap
HashMap为什么线程不安全?1、哈希值冲突,因为哈希值会得出存在同样的数组下标,存储到链表的时候,若多线程同时插入链表头或者尾,另一个线程的存储的下标会丢失。
2、在扩容时,当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,
结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失,但最后个重设不一定适合其他的线程。
可以使用hashtable或者Collections.synchronizedMap方法,他们会给集合整体上锁,导致其他线程的读取都堵塞,读取性能变差。
ConcurrentHashMap 集合中有多个Segment,一个Segment可以理解为一个HashMap,Segment也包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。ConcurrentHashMap是一个双层哈希表。每个segment的读写是高度自治的,segment之间互不影响。这称之为“锁分段技术”。
由于分层,导致计算其size比较繁琐,遍历所有的segment,计算其修改次数和元素量。
应用举例:
Map visitors = (Map)getServletContext().getAttribute("visitors");
if(visitors == null){
visitors = new ConcurrentHashMap();
getServletContext().setAttribute("visitors", visitors);
}
String clickIp = request.getRemoteAddr();//用户IP
String key = articleId+"_"+clickIp; //以文章ID和用户IP为键
Date lastVisitTime = (Date)visitors.get(key);
Article a = articleDao.findArticleById(Integer.parseInt(articleId));
int clickNumber = a.getClickNumber(); //旧的点击量
//没有访问记录、或最后一次访问在一个小时之前,需再次记录访问量,否则,无需再次记录访问量
if(lastVisitTime == null || !withinOneHour(lastVisitTime)){
//更新点击量
clickNumber = articleDao.updateClickNumber(Integer.parseInt(articleId));
visitors.put(key, new Date());
}