数据结构:html
数组 + 单链表node
transient Entry[] table; // 数组 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;// 单链表,存储hash冲突的对象 final int hash;
hash桶的计算:算法
首先把hash桶的个数适配到2的n次方shell
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;//把容量适配到2^n,方便后面的 this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity];
hash桶的查找经过hashcode与(桶个数-1)进行按位与操做,至关于取模,可是效率要高。数组
static int indexFor(int h, int length) { return h & (length-1); }
为何要把hashmap的容量适配到2的n次方呢?由于2^n-1正好各个位都是1,这样在按位与操做时其结果彻底取决于hashcode,只要hashcode算法得当,就可使得hash桶的数据分布比较均匀。若是容量不是2的n次方的话,就会出现0的位,会致使进行与操做后有些桶就一直放不进数据的状况。安全
旋转hash:在原有hashcode的基础上再hash一次,充分利用高位进行计算,减小因低位相同的状况致使的hash碰撞。数据结构
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
扩容:并发
当数据量达到阀值(capacity * loadFactor)时,为了减小数据量增长带来的hash碰撞,须要对HashMap进行扩容。须要把全部的entry移动一次,代价较大,因此在能够预估容量的时候尽可能在初始化时指定容量。高并发
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
fail-fast:性能
HashMap是非线程安全的集合,在进行HashMap遍历(HashIterator)操做时,若是map有修改操做,都会增长modCount的值,经过modCount与expectModCount进行比较,若是二者不相等,当即抛出ConcurrentModificationException。
数据结构:
数组 + 单链表 + 双向链表
LinkedHashMap是在HashMap的基础上添加了一个双向链表结构,来按必定的顺序维护里面的全部entry。
Entry<K,V> before, after;//双向链表结构 header = new Entry<K,V>(-1, null, null, null); header.before = header.after = header;
顺序性:
提供两种顺序方式来维护entry:
按插入有序:Insertion-Ordered,全部entry按插入的顺序排序,读取的时候老是从最早插入的那个entry开始读取。
按访问有序:Access-Ordered(全部entry从least-recently到most-recently再到header排列)。entry每次被访问都要调整它的顺序,从新放到header结点前面,这样一直不被访问的entry就离header愈来愈远。
这是怎么作到的呢?LinkedHashMap的添加结点操做都是addBefore,并且每次都是在header结点以前进行插入(其实这里面的head结点是个尾结点)离尾结点最远的就是最老的结点(header.after指向),这是个逆向链,因此遍历的时候从header.after开始,就能取到按插入有序的数据。
private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; }
遍历操做,从header.after开始遍历。
Entry<K,V> nextEntry = header.after; Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (nextEntry == header) throw new NoSuchElementException(); Entry<K,V> e = lastReturned = nextEntry; nextEntry = e.after; return e; }
LRU
LinkedHashMap自然支持LRU操做,即Access-Ordered,默认不开启。
protected boolean removeEldestEntry(Map.Entry eldest) { return false;// 默认是返回false,咱们能够实现这个方法来支持LRU }
其余
非线程安全,fast-fail(同hashmap)
ConcurrentHashMap采用了锁分离的技术来实现确保线程安全的状况下达到较好的性能。它把整个hash table分红好多个小的hash table(即Segment),每一个Segment都有本身的锁来保证线程安全,这样就使得各个Segment均可以独立地进行管理,而不须要争用锁。
两个重要的结构:HashEntry和Segment
HashEntry中,value被申明为volatile,这样保证了value的可见性,并发访问时不会出现脏数据。next被申请为final,保证了链表的中间和结尾部分都不会改变,进行读操做时就不须要加锁,这样能够提升并发性。
final K key; final int hash; volatile V value; final HashEntry<K,V> next; // 下面这句是put操做的行为,也就是每次put都是往头节点前面插入新节点,不影响原来的链表结构。 tab[index] = new HashEntry<K,V>(key, hash, first, value);
Segment继承了ReentrantLock,天生具备锁的功能,因此在put或remove操做时能够直接加锁使用。
扩容(rehash)操做
若是原hash桶的链表里的全部结点rehash值都同样,直接把链表链接到新桶上便可;
不然就找到链表尾部相同rehash值的子链表,直接链接到新桶上(代码片断一),这样保证rehash到同一个桶的多个节点不会出现连接顺序反转的状况,也避免了像HashMap那样在高并发下rehash出现死循环的现象(http://coolshell.cn/articles/9606.html)。
最后把当前子链表前面的那部分节点正常计算rehash并添加到新桶的位置(代码片断二)。
// 代码片断一 // Reuse trailing consecutive sequence at same slot HashEntry<K,V> lastRun = e; int lastIdx = idx; for (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } newTable[lastIdx] = lastRun;
这里解释一下上面这行代码newTable[lastIdx] = lastRun
, 刚开始在怀疑直接给新桶赋值的话会不会被后面的entry给覆盖掉?仔细想一想,彻底不必担忧这个。举个例子,segment(小hash表)容量从32扩充到64的状况,也就是容量从2^5=100000(掩码是:011111)扩充到2^6=1000000(掩码是:[1]11111)这个[]里的1就是扩充后新增的位,能够想象,在原容量下的entry,大部分都不会rehash到新桶里,只有[]指示的位是1的状况才会rehash到新桶里面,因此rehash操做移动链表上一半的元素到新桶里。另外,原容量下不一样桶里面的元素,rehash后也不会出如今相同的桶里面,其位置仍是取决于非[]指示的位置,跟原容量下的同样。因此上面这个操做能够直接连接过去,没必要担忧重复被覆盖的状况。
// 代码片断二 // Clone all remaining nodes for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { int k = p.hash & sizeMask; HashEntry<K,V> n = newTable[k]; newTable[k] = new HashEntry<K,V>(p.key, p.hash, n, p.value); }
size()操做
size操做会先尝试两次不加锁的状况下计算全部segment的size总数,若是两次计算的结果相等,说明size是正确的,直接返回这个结果。若是不相等,则全部segment加锁作一次计算。
推荐阅读: