一、HashMap
1.HashMap本质是一个数组,数组的每个元素都是一个单链表。
java源码中,这个数组就是table,其定义如下:
transient Node<K,V>[] table;//table数组,每个数组元素都是一个链表,链表由0个或多个节点组成
节点类定义如下,注释中解释此类:
//静态内部类的特点:在创建静态内部类的实例时,不必创建外部类的实例
static class Node<K,V> implements Map.Entry<K,V> {//Entry是Map接口中的一个内部接口
final int hash;//此节点的哈希值,同一个链表上的哈希值不一定相同
final K key;//键,不能修改
V value;//值
Node<K,V> next;//指向下一个节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {//此Node类的hashCode方法
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {//重新设置节点Value,返回旧Value
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {//判断节点相等的方法,
if (o == this)//同一个对象,返回true
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;//键和值都相等则返回true
}
return false;
}
}
2.HashMap的重点方法
(1)hash方法
hash()用来计算一个键对应的hash值,
(HashMap中的hashCode方法用来返回HashMap对象的hash值,跟这里研究的hash()没有一毛钱关系)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key.hashCode()函数调用的是key键值类型自带的哈希函数,它返回一个32位int类型的散列值。
考虑到2进制32位带符号的int表值范围从-2147483648到2147483648,前后加起来大概40亿的映射空间。一个40亿长度的数组,内存是放不下的!
所以散列值一般只会用到后面的位,但是如果只取到最后几位的话,碰撞会很严重。于是就有了“扰动函数”——右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
hash()的返回值是一个低16位经过扰动处理的int类型,还是不会直接拿来用的。源码中每次使用键的hash值时都会通过这种方式:
n = tab.length
tab[(n - 1) & hash]
由resize()方法(Initializes or doubles table size)可知,数组长度必为2的整数次幂,因此(n - 1) & hash相当于低位掩码。
那么为什么不用取余呢?因为散列值的大小必须在[0,length-1]中,而取余的结果可能是负数。
那么为什么不用取余再取绝对值呢?因为对于Integer.MIN,Math.abs()会返回一个负值。
另外,从这里也可以得出一个结论:对于HashMap的同一个链表中各个节点的key的hash值不一定相同。
(2)get()
//获取键对应的值
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//根据哈希值和键获取对应的值对象
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {//先通过hash值找到对应的链表头节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))//如果要找的key和第一个node的key是同一个对象or equals,则返回第一个节点
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)//如果此链表节点类型为红黑树节点,则以遍历红黑树的方式搜索节点
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//遍历链表
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//如果hash值相等且key对象是同一个或equals,返回
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HashMap在JDK1.8中新增的操作:桶的树形化——在Java 8 中,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度。
- TREEIFY_THRESHOLD
- UNTREEIFY_THRESHOLD
- MIN_TREEIFY_CAPACITY
值及作用如下:
//一个桶的树化阈值
//当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
//这个值必须为 8,要不然频繁转换效率也不高
static final int TREEIFY_THRESHOLD = 8;
//一个树的链表还原阈值
//当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
//这个值应该比上面那个小,至少为 6,避免频繁转换
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表的最小树形化容量
//当哈希表中的容量大于这个值时,表中的桶才能进行树形化
//否则桶内元素太多时会扩容,而不是树形化
//为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
(3)put()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//table不存在则先初始化之
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//如果链表为null则新建一个节点(nextNode指向自己)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//验证要存的键值对是否等于头节点
e = p;
else if (p instanceof TreeNode)//处理数组该处元素为树的情况
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//遍历链表插入<K,V>
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//不存在此key,新加入一个节点并验证是否需要树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//存在此key
break;
p = e;//相当于p = p.next
}
}
if (e != null) { //处理存在此key的情况:更新value并返回旧value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);// Callbacks to allow LinkedHashMap post-actions
return oldValue;
}
}
++modCount;//modCount用于记录HashMap的修改次数
/**由于HashMap不是线程安全的,所以在迭代的时候,会将modCount赋值到迭代器的expectedModCount属性中,然后进行迭代,
*如果在迭代的过程中HashMap被其他线程修改了,modCount的数值就会发生变化,
*这个时候expectedModCount和ModCount不相等,
*迭代器就会抛出ConcurrentModificationException()异常
**/
if (++size > threshold)//数组大小到达临界值则会double size
resize();
afterNodeInsertion(evict);// Callbacks to allow LinkedHashMap post-actions
return null;//key存在时返回oldValue,不存在时返回null
}
4.hashmap例题
https://www.cnblogs.com/coderxuyang/p/3718856.html
初始容量设为400/3=134,hashmap会自动变为大于134的最小2^n,即256.
二、ConcurrentHashMap
// ConcurrentHashMap核心数组
transient volatile Node<K,V>[] table;
// 扩容时才会用的一个临时数组
private transient volatile Node<K,V>[] nextTable;
/**
* table初始化和resize控制字段
* 负数表示table正在初始化或resize。-1表示正在初始化,-N表示有N-1个线程正在resize操作
* 当table为null的时候,保存初始化表的大小以用于创建时使用,或者直接采用默认值0
* table初始化之后,保存下一次扩容的的大小,跟HashMap的threshold = loadFactor*capacity作用相同
*/
private transient volatile int sizeCtl;
// resize的时候下一个需要处理的元素下标为index=transferIndex-1
private transient volatile int transferIndex;
// 通过CAS无锁更新,ConcurrentHashMap元素总数,但不是准确值
// 因为多个线程同时更新会导致部分线程更新失败,失败时会将元素数目变化存储在counterCells中
private transient volatile long baseCount;
// resize或者创建CounterCells时的一个标志位
private transient volatile int cellsBusy;
// 用于存储元素变动
private transient volatile CounterCell[] counterCells;
2.CAS(比较并交换)
Unsafe.compareAndSwapXXX方法是jdk.internal.misc.Unsafe类中的方法,Unsafe类用于执行低级别、不安全操作的方法集合。
private static final Unsafe U = Unsafe.getUnsafe();
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
CAS的含义是:“我认为V的值应该是A,如果是,那么将V的值更新为B;否则,不修改并告诉V的值实际是多少"
CAS方法都是native方法,可以保证原子性,并且效率比synchronized高。
3.spread方法
ConcurrentHashMap中没有hash()方法,取而代之的是spread方法。spread方法解释:
static final int HASH_BITS = 0x7fffffff;//用来屏蔽符号位
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;//先对低16位进行扰动处理,然后屏蔽符号位,结果为32位int型非负数
}
int h = spread(key.hashCode());//调用
e = tabAt(tab, (n - 1) & h)//和hash()一样,不会直接使用,根据数组长度只取低位哈希值
4.get()
get操作不需要锁。除非读到的值是空的才会加锁重读。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);//获取传入的哈希值对应的节点
}
/**
*数组元素定位:
*
*Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,
*这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。
*Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,
*这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,
*可以获取数组的转换因子,也就是数组中元素的增量地址。
*将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。
**/
ABASE = U.arrayBaseOffset(Node[].class);//获取数组第一个元素的偏移地址
int scale = U.arrayIndexScale(Node[].class);//获取数组中元素的增量
if ((scale & (scale - 1)) != 0)//scale不是2的整数次方则出错
throw new Error("array index scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);//scale非0位的位数
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
有点看不下去了,以后再写吧。
LinkedHashMap
这篇不错:
https://blog.csdn.net/justloveyou_/article/details/71713781
linkedhashmap的特色是有序(插入顺序或读取顺序),考点是LRU!!
LinkedHashMap 与 LRU(Least recently used,最近最少使用)算法
到此为止,我们已经分析完了LinkedHashMap的存取实现,这与HashMap大体相同。LinkedHashMap区别于HashMap最大的一个不同点是,前者是有序的,而后者是无序的。为此,LinkedHashMap增加了两个属性用于保证顺序,分别是双向链表头结点header和标志位accessOrder。我们知道,header是LinkedHashMap所维护的双向链表的头结点,而accessOrder用于决定具体的迭代顺序。实际上,accessOrder标志位的作用可不像我们描述的这样简单,我们接下来仔细分析一波~
我们知道,当accessOrder标志位为true时,表示双向链表中的元素按照访问的先后顺序排列,可以看到,虽然Entry插入链表的顺序依然是按照其put到LinkedHashMap中的顺序,但put和get方法均有调用recordAccess方法(put方法在key相同时会调用)。recordAccess方法判断accessOrder是否为true,如果是,则将当前访问的Entry(put进来的Entry或get出来的Entry)移到双向链表的尾部(key不相同时,put新Entry时,会调用addEntry,它会调用createEntry,该方法同样将新插入的元素放入到双向链表的尾部,既符合插入的先后顺序,又符合访问的先后顺序,因为这时该Entry也被访问了);当标志位accessOrder的值为false时,表示双向链表中的元素按照Entry插入LinkedHashMap到中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序。因此,当标志位accessOrder的值为false时,虽然也会调用recordAccess方法,但不做任何操作。
注意到我们在前面介绍的LinkedHashMap的五种构造方法,前四个构造方法都将accessOrder设为false,说明默认是按照插入顺序排序的;而第五个构造方法可以自定义传入的accessOrder的值,因此可以指定双向循环链表中元素的排序规则。特别地,当我们要用LinkedHashMap实现LRU算法时,就需要调用该构造方法并将accessOrder置为true。
参考:
http://www.importnew.com/16301.html
https://www.zhihu.com/question/20733617/answer/111577937
https://blog.csdn.net/u011240877/article/details/53358305
https://www.cnblogs.com/snowater/p/8087166.html
https://www.cnblogs.com/mickole/articles/3757278.html
http://www.bubuko.com/infodetail-1612665.html