本篇博客,将介绍java(jdk1.8)基础中的一些集合类的实现原理,及对相应代码进行分析。主要对Map,List相关的类进行分析。
- Map
Map接口的总体相关类图,(看不清可在新标签中查看)
- HashMap
HashMap的底层实现的主要数据结构如下,以Node<K,V>[]数组为桶,每个Node后面长度小于8为链表,否则就为红黑树。数据结构为数组+链表+红黑树。如下图(看不清可在新页签查看)
这时介绍一下红黑树,首先红黑树是一个平衡二叉树,交且具有以下性质:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
理解HashMap的原理,主要从源码中的hash,putVal,getNode这三个方法入手
hash方法用于构造hash值,如下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
接下来,看HashMap添加元素的时候,是如何构建整个数据结构的。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //找到hash的桶的位置,没有值就创建新的Node if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { HashMap.Node<K,V> e; K k; //判断和入参的key,value是否匹配 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof HashMap.TreeNode)//如果是个树节点,则把入参的key,value构建到树中 e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) {//binCount统计这个链表长度有多少 if ((e = p.next) == null) {//往链表最后面插入新的节点 p.next = newNode(hash, key, value, null); //如果链表长度大于等于7,构造为树 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)))) break; p = e; } } //入参的key值在原先的桶中已有对应的value值, // 根据参数onlyIfAbsent或原先值是否为null来判断是否覆盖新入参的value值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //留给LinkedHashMap扩展使用 afterNodeAccess(e); return oldValue; } } ++modCount;//标识被修改过了 //是否需要扩容 if (++size > threshold) resize(); //留给LinkedHashMap扩展使用 afterNodeInsertion(evict); return null; }
final HashMap.Node<K,V> getNode(int hash, Object key) { HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && //通过hash值找到对应桶 (first = tab[(n - 1) & hash]) != null) { //桶的值就是要找的值,直接返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { //是树节点的话,遍历树查找 if (first instanceof HashMap.TreeNode) return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key); //为链表,就遍历链表查找值 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }获取元素跟添加相对的,找到桶的初始位置,要么遍历链表,要么遍历树,找到要找的key,返回value值。
- HashTable
通常会拿HashTable与HashMap进行比较,我们先来看下HashTable添加元素和获取元素的过程。
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Hashtable.Entry<?,?> tab[] = table; int hash = key.hashCode(); //获取桶的位置 int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Hashtable.Entry<K,V> entry = (Hashtable.Entry<K,V>)tab[index]; //遍历链表,找到key对应的value值,返回用新值覆盖旧值,并返回旧值 for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } //没有找到,就往链表头新增一个Entry addEntry(hash, key, value, index); } private void addEntry(int hash, K key, V value, int index) { modCount++; Hashtable.Entry<?,?> tab[] = table; //判断是否需要扩容 if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. @SuppressWarnings("unchecked") //往链表头插入新的元素 Hashtable.Entry<K,V> e = (Hashtable.Entry<K,V>) tab[index]; tab[index] = new Hashtable.Entry<>(hash, key, value, e); count++; }
public synchronized V get(Object key) { Hashtable.Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; //从hash值对应的桶的位置开始查找链表里是否有key,有直接返回value,没有返回null for (Hashtable.Entry<?,?> e = tab[index]; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; }
从hash值对应的桶的位置开始查找链表里是否有key,有直接返回value,没有返回null。这个过程就比较简单,但是问题也很明显,如果链表很长,HashTable查找的效率就会退化成链表的效率了。这也是一个线程安全的方法。
- LinkedHashMap
LinkedHashMap继承HashMap,也就是说它的底层数据结构跟HashMap是一样的。LinkedHashMap相当于是对HashMap的扩展,具体表现在两个方面:1、用一个链表维护其顺序(存取顺序或插入顺序)2、是否要启用LRU功能。
首先看下维护顺序的实现:
void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; //按存取顺序,把要找的node放到维护顺序的链表的最后 if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; //这里要使用LRU功能,需要继承linkedHashMap,重写removeEldestEntry方法 if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; //删除节点的具体操作 removeNode(hash(key), key, null, false, true); } }这里的removeEldestEntry方法在LinkedHashMap中总是返回false,所以如果要使用LRU功能,需继承LinkedHashMap,重写removeEldestEntry方法。removeNode就是具体删除节点的操作。(这里的LRU实现,如果对Dubbo熟悉的话,会发现dubbo的LRUCache就是继承LinkedHashMap实现的。
- CAS
CAS实现的是一种区别于synchronouse的乐观锁。其底层实现是依靠硬件的Cmpxchg原子指令,保证内存的可靠性。
java中的原子类实现,都是基于CAS实现的。以AtomicInteger为例子,分析下其实现代码。
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }AtomicInteger类的compareAndSet方法,其实是调用了unsafe类的compareAndSwarInt方法,这个方法有四个参数,分别是表示:第一个参数this,要修改的对象;第二个参数valueOffset,要修改的属性在内存中的偏移量;第三个参数expect,期待的要修改属性在内存中的值;第四个参数update,要更新的值;具体实现逻辑是,只在当要修改的属性在内存中的值与expect的值一样,才把这个属性值修改为update的值,否则不做改变。
再看下AtomicInteger的getAndSet方法
public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue); }
在unsafe中的实现为:
public final int getAndSetInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var4)); return var5; }可以看到,unsafe里,如果cas失败,则会一直重试直到成功。这里使用cas固然比使用synchronouse锁要好,但是cas也有以下几个缺点:
1、ABA问题:
如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
要解决这个问题,其思路是添加一个版本号,比较原先是A1->B1->A2,这样就把A1和A2区分开了。java里面用AtomicStampedReference这个类来解决ABA问题,其实现原理是增中一个时间戳来充当版本号。根据原先值和时间戳还判断是否改变。
2、自旋时间长开销大
这个问题就跟刚刚说的AtomicInteger的getAndSet方法实现一样,底层unsafe实现会循环去cas,直到成功为止。
3、只能保证一个共享变量的原子操作
这个问题就是cas只对一个属性进行操作,如果要同时对A,B两个属性保证他们原子性,就是AB要同时修改值,不能A成功B失败。这个问题在java里使用AtomicReference类来保证对象的原子性,前面说的那个例子,可以把A,B封装成一个对象,再用AtomicReference保证这个对象的原子性。
- ConcurrentHashMap(jdk1.8)
ConcurrentHashMap是线程安全的HashMap,jdk1.8这个类的实现方式,相比于jdk1.7的分段锁的悲观锁,改为使用CAS乐观锁,CAS的实现原理上文大概介绍过。ConcurrentHashMap底层数据结构也是数组+链表+红黑树。这里我们主要分析putVal,get及其相关的方法。首先先介绍保证原子性的主要方法:
/** * 获取i位置上的Node节点 * @param tab * @param i * @param <K> * @param <V> * @return */ static final <K,V> ConcurrentHashMap.Node<K,V> tabAt(ConcurrentHashMap.Node<K,V>[] tab, int i) { return (ConcurrentHashMap.Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); } /** * 利用cas算法去修改i位置上的node节点 * @param tab * @param i * @param c * @param v * @param <K> * @param <V> * @return */ static final <K,V> boolean casTabAt(ConcurrentHashMap.Node<K,V>[] tab, int i, ConcurrentHashMap.Node<K,V> c, ConcurrentHashMap.Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); } /** * 利用volatile方法设置i位置上node节点 * @param tab * @param i * @param v * @param <K> * @param <V> */ static final <K,V> void setTabAt(ConcurrentHashMap.Node<K,V>[] tab, int i, ConcurrentHashMap.Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); }
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (ConcurrentHashMap.Node<K,V>[] tab = table;;) { ConcurrentHashMap.Node<K,V> f; int n, i, fh; //tab为空,初始化table if (tab == null || (n = tab.length) == 0) tab = initTable(); //tabAt获取i位置上的node节点,如果为空 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //cas修改i位置上的node节点 if (casTabAt(tab, i, null, new ConcurrentHashMap.Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //static final int MOVED = -1; // hash for forwarding nodes //扩容相关处理 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) {//对要找的hash相同的位置上的节点加锁 if (tabAt(tab, i) == f) { // static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash //是链表结点 if (fh >= 0) { binCount = 1; for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) { K ek; //找到相同key,根据onlyIfAbsent参数看是否替换 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } //没有找到key就往链表最后加入新节点 ConcurrentHashMap.Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new ConcurrentHashMap.Node<K,V>(hash, key, value, null); break; } } } //如果是红黑树节点 else if (f instanceof ConcurrentHashMap.TreeBin) { ConcurrentHashMap.Node<K,V> p; binCount = 2; //根据红黑树找到相同key的节点,根据onlyIfAbsent参数决定是否替换值 if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } //如果链表的长度大于8,就构建为红黑树 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } //长度增加1 addCount(1L, binCount); return null; }这里在获取节点和设置节点都是无锁实现线程安全,这里增加元素的操作也基本是跟HashMap差不多。但是多线程的扩容就比较复杂了。
synchronized (f) { if (tabAt(tab, i) == f) { ConcurrentHashMap.Node<K,V> ln, hn; if (fh >= 0) { int runBit = fh & n; ConcurrentHashMap.Node<K,V> lastRun = f; for (ConcurrentHashMap.Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } if (runBit == 0) { ln = lastRun; hn = null; } else { hn = lastRun; ln = null; } for (ConcurrentHashMap.Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) ln = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, ln); else hn = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, hn); } setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; }先看下链表的扩容,假设原table的如下:
按 hash&n 分为蓝红两种节点,当前f是14号桶。那么14号的扩容过程如下:
1、通过遍历链表,记录runBit和lastRun,分别为1和节点6,所以设置hn为节点6,ln为null;
2、重新遍历链表,以lastRun节点为终止条件,根据第X位的值分别构造ln链表和hn链表:
2、重新遍历链表,以lastRun节点为终止条件,根据第X位的值分别构造ln链表和hn链表:
for (ConcurrentHashMap.Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) ln = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, ln); else hn = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, hn); }
ln链:和原来链表相比,顺序已经不一样了
hn链:
通过CAS把ln链表设置到新数组的i位置,hn链表设置到i+n的位置;
setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true;
接下来分析get方法:
public V get(Object key) { ConcurrentHashMap.Node<K,V>[] tab; ConcurrentHashMap.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) { //如果对应位置的桶就是要找的key,直接返回 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; } } //找不到返回null return null; }查找过程跟HashMap大致相同,只不过是通过tabAt无锁获取节点。之后要么遍历红黑树查找,要么遍历链表查找,找不到返回null。
- List
List的类图如下:(可在新页签查看更清楚)
- ArrayList
在新增元素前要进行检查是否需要扩容
public boolean add(E e) { //在增加元素之前需要检查是否需要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
具体实现扩容的代码:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //相当于原来的3/2 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
容量扩展成原来的3/2,并且检查上下限,没有超过就把原先的数据复制到新的数组中。
- LinkedList
LinkedList添加元素比较简单,就是直接往链表后面添加
public boolean add(E e) { linkLast(e); return true; }
添加过程中记录modCount
void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }比较ArrayList与LinkedList:ArrayList相比LinkedList查询速度比较快,但是不适合频繁添加删除元素。LinkedList则刚好相反,适合频繁添加删除元素,但是查询速度较慢。