概览 Collection
List:
ArrayList 基于动态数组(动态扩容),支持随机访问
Vector 线程安全
LinkedList 基于双向链表,只能顺序访问,但可快速插入和删除,还可用作栈、队列和双向链表
Set
HashSet 哈希表实现,快速查找,不支持有序性操作
TreeSet 红黑树实现,支持有序性操作
LinkedHashSet 具有hashSet的查找效率,内部使用双向链表维护插入顺序
Queue
LinkedList ,双向队列,双向链表实现
PriorityQueue 优先队列,堆实现
Map
HashMap 基于哈希表实现
HashTable 线程安全,实现同HashMap
TreeMap 红黑树实现
LinkedHashMap 哈希表实现,使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序
容器中的设计模式 迭代器 Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历Collection 中的元素。
适配器 java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
源码分析 ArrayList 概览 ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。
数组默认大小是10.
扩容 添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1) ,也就是旧容量的 1.5 倍。
扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public boolean add (E e) { ensureCapacityInternal(size + 1 ); elementData[size++] = e; return true ; } private void ensureCapacityInternal (int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity (int minCapacity) { modCount++; if (minCapacity - elementData.length > 0 ) grow(minCapacity); } private void grow (int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1 ); if (newCapacity - minCapacity < 0 ) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0 ) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
删除元素 需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。
1 2 3 4 5 6 7 8 9 10 public E remove (int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1 ; if (numMoved > 0 ) System.arraycopy(elementData, index+1 , elementData, index, numMoved); elementData[--size] = null ; return oldValue; }
Fail-Fast modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出ConcurrentModificationException。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException { int expectedModCount = modCount; s.defaultWriteObject(); s.writeInt(size); for (int i=0 ; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
序列化 保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
使用
1 2 3 ArrayList list = new ArrayList(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(list);
###
Vector 与ArrayList实现相似,但用了synchronized进行同步。
与 ArrayList 的比较
Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步 操作完全可以由程序员自己来控制;
Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
替代方案
Collections.synchronizedList(); 得到一个线程安全的 ArrayList.
使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
CopyOnWriteArrayList 读写分离 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。 写操作需要加锁,防止并发写入时导致写入数据丢失。 写操作结束之后需要把原始数组指向新的复制数组。
适用场景 CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是 CopyOnWriteArrayList 有其缺陷:
内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
LinkedLsit 基于双向链表实现,使用 Node 存储链表节点信息。
1 2 3 4 5 private static class Node <E > { E item; Node<E> next; Node<E> prev; }
每个链表存储了 first 和 last 指针:
1 2 transient Node<E> first;transient Node<E> last;
与ArrayList比较
ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
ArrayList 支持随机访问,LinkedList 不支持;
LinkedList 在任意位置添加删除元素更快。
HashMap 概览 hashMap使用基于拉链的散列算法,jdk1.8起使用红黑树优化过长的链表。底层结构为数组+链表+红黑树。
源码分析 构造函数 比较关键的在于一些属性值得设置,初始容量、负载因子、阈值。
threshold = capacity * loadFactor
MAXIMUM_CAPACITY
名称
用途
默认值
initialCapacity
HashMap 初始容量
16
loadFactor
负载因子
0.75
threshold
当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容
通过计算
MAXIMUM_CAPACITY
最大容量
1<<30
TREEIFY_THRESHOLD
链表树化阈值
8
UNTREEIFY_THRESHOLD
链表化的阈值(小于这个值,用链表形式)
6
MIN_TREEIFY_CAPACITY
最小容量
64
查找 原理
1. 根据hash定位槽
2. 查找给定key(hash相等,key相等),依次判断,找到直接返回,否则最后返回null
1. 桶元素
2. 若桶元素为树节点类型,委托给树查找元素
3. 在链表中查找
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 ) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) 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)))) return e; } while ((e = e.next) != null ); } } return null ; }
遍历 从index = 0, table[index]
开始,找到一个不为null的slot,遍历链表,用到的是next引用。
插入 原理:根据hash定位到槽,如果槽为空,直接插入,否则连接到链表尾部、或更新键值对
涉及到table 初始化、扩容、树化。
当桶数组 table 为空时,通过扩容的方式初始化 table
查找要插入的键值对是否已经存在,如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树
如果已存在,根据条件判断是否用新值替换旧值
判断键值对数量是否大于阈值,大于的话则进行扩容操作
扩容 原理
计算新容量newCap和新阈值newThr
创建新的数组
将键值对重新映射到新数组上
如果无链表,直接根据hash&(newCap-1)定位
如果是树节点,委托红黑树来拆分和重新映射
为链表,根据hash&oldCap的值分成两组,映射到j和j+oldCap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; if (oldCap > 0 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0 ) { float ft = (float )newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float )MAXIMUM_CAPACITY ? (int )ft : Integer.MAX_VALUE); } threshold = newThr; Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null ) { for (int j = 0 ; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null ) { oldTab[j] = null ; if (e.next == null ) newTab[e.hash & (newCap - 1 )] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this , newTab, j, oldCap); else { Node<K,V> loHead = null , loTail = null ; Node<K,V> hiHead = null , hiTail = null ; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0 ) { if (loTail == null ) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null ) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null ); if (loTail != null ) { loTail.next = null ; newTab[j] = loHead; } if (hiTail != null ) { hiTail.next = null ; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
######
链表树化
链表树化有两个条件,不满足采用扩容,满足再扩容
树化时,将Node节点替换为TreeNode,保留next信息
替换后,再从head开始,进行红黑树化(标记红黑节点、父子节点,如果root节点不是first节点,再修正next和prev?)【链表转成红黑树后,原链表的顺序仍然会被引用仍被保留了(红黑树的根节点会被移动到链表的第一位)】
在扩容过程中,树化要满足两个条件:
链表长度大于等于 TREEIFY_THRESHOLD 8
桶数组容量大于等于 MIN_TREEIFY_CAPACITY 64
红黑树拆分 红黑树中保留了next引用,拆分原理和链表相似
根据hash拆分成两组(这时候会生成新的next关系)
各组内根据情况,链化或者重新红黑树化
红黑树链化 将TreeNode替换为Node
删除 原理
定位到槽
找到删除节点
删除节点,并修复链表或红黑树
ConcurrentHashMap 相比较HashMap,主要是新增了写操作时候的同步处理
扩容迁移时,可以多个线程帮助迁移
Q:ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
①、粒度降低了 ; ②、优化后的synchronized性能与ReentrantLock不相上下,基于JVM也保证synchronized在各平台上都可使用。 ③、在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存 。
插入
计算hash
循环执行
如果数组为空,初始化initTable
如果hash定位到的槽为空,CAS替换为新节点,退出循环
如果槽不为空,节点hash为-1,说明正在迁移,helpTransfer
槽不为空,且不在迁移,那么,对头节点加监控器锁,链表或红黑树形式插入或更新节点
addCount
计数【扩容==迁移】 主要为putVal情形下的addCount,主要逻辑:
更新as的值,如果有竞争,退出方法,无竞争,根据as和baseCount统计当前节点个数s
如果s大于扩容阈值
如果sizeCtl<0,说明正在迁移。如果未到迁移上限、或者迁移结束、或者新表未创建,退出本次循环,否则更新迁移线程数后进入迁移。
如果sizeCtl>=0,由当前线程启动迁移,CAS更新sizeCtl为(rs << RESIZE_STAMP_SHIFT) + 2)
,进入迁移。
再sunCount统计s个数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private final void addCount (long x, int check) { CounterCell[] as; long b, s; if ((as = counterCells) != null || !U.compareAndSwapLong(this , BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true ; if (as == null || (m = as.length - 1 ) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return ; } if (check <= 1 ) return ; s = sumCount(); } if (check >= 0 ) { Node<K,V>[] tab, nt; int n, sc; while (s >= (long )(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); if (sc < 0 ) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0 ) break ; if (U.compareAndSwapInt(this , SIZECTL, sc, sc + 1 )) transfer(tab, nt); } else if (U.compareAndSwapInt(this , SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2 )) transfer(tab, null ); s = sumCount(); } } }
迁移 transfer的第二个参数为空的时候,触发扩容,创建nextTable,在addCount和tryPresize中有这样的调用。
addCount是size不精确情况下,可能触发扩容;tryPresize是已知精确size的情况下做扩容。
总结下流程
计算步长stride
如果nextTab未创建,则创建之,并赋给nextTable
循环迁移
分配迁移区间i和
bound(
i从前往后,
bound = i - stride + 1`,总之就是stride)
如果区间已达边界,将sc减1,表示本线程退出迁移。如果是最后一个迁移线程,标记finish和advance为true,进入下一循环recheck;非最后线程,直接退出方法。
若未达边界,且槽为空,CAS槽为fwd,进入下一循环
槽不为空,且槽已经是fwd,进入下一循环
最后一种情形,进行迁移
为链表,根据节点hash二进制第k位为0或1分成两组(n=2^k),1连接到高位槽上
为红黑树,分组同链表,分好的组根据节点个数判断是否链化或新生成红黑树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 private final void transfer (Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; if ((stride = (NCPU > 1 ) ? (n >>> 3 ) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; if (nextTab == null ) { try { @SuppressWarnings ("unchecked" ) Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1 ]; nextTab = nt; } catch (Throwable ex) { sizeCtl = Integer.MAX_VALUE; return ; } nextTable = nextTab; transferIndex = n; } int nextn = nextTab.length; ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); boolean advance = true ; boolean finishing = false ; for (int i = 0 , bound = 0 ;;) { Node<K,V> f; int fh; while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) advance = false ; else if ((nextIndex = transferIndex) <= 0 ) { i = -1 ; advance = false ; } else if (U.compareAndSwapInt (this , TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0 ))) { bound = nextBound; i = nextIndex - 1 ; advance = false ; } } if (i < 0 || i >= n || i + n >= nextn) { int sc; if (finishing) { nextTable = null ; table = nextTab; sizeCtl = (n << 1 ) - (n >>> 1 ); return ; } if (U.compareAndSwapInt(this , SIZECTL, sc = sizeCtl, sc - 1 )) { if ((sc - 2 ) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return ; finishing = advance = true ; i = n; } } else if ((f = tabAt(tab, i)) == null ) advance = casTabAt(tab, i, null , fwd); else if ((fh = f.hash) == MOVED) advance = true ; else { synchronized (f) { if (tabAt(tab, i) == f) { Node<K,V> ln, hn; if (fh >= 0 ) { int runBit = fh & n; Node<K,V> lastRun = f; for (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 (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 Node<K,V>(ph, pk, pv, ln); else hn = new Node<K,V>(ph, pk, pv, hn); } setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true ; } else if (f instanceof TreeBin) { TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> lo = null , loTail = null ; TreeNode<K,V> hi = null , hiTail = null ; int lc = 0 , hc = 0 ; for (Node<K,V> e = t.first; e != null ; e = e.next) { int h = e.hash; TreeNode<K,V> p = new TreeNode<K,V> (h, e.key, e.val, null , null ); if ((h & n) == 0 ) { if ((p.prev = loTail) == null ) lo = p; else loTail.next = p; loTail = p; ++lc; } else { if ((p.prev = hiTail) == null ) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0 ) ? new TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0 ) ? new TreeBin<K,V>(hi) : t; setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true ; } } } } } }
####
查找 LinkedHashMap 概览
Entry继承自HashMap.Entry,增加before和after引用
增加head和tail引用
默认维护插入顺序,accessOrder = false。构造函数中允许传入accessOrder = true,维护访问顺序(最近访问的移动到tail),实现LRU(Least recently used,最近最少使用)策略的缓存。
新增节点newNode时,若tail为空,head指向当前节点,tail指向当前节点。一般在put时候调用newNode方法。
1 2 3 4 5 6 Node<K,V> newNode (int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; }
1 2 3 4 5 6 7 8 9 10 private void linkNodeLast (LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null ) head = p; else { p.before = last; last.after = p; } }
删除节点时,将当前节点的before和after建立连接
维护访问顺序时,构造函数中传入accessOrder = true,在调用get/getOrDefault/replace等方法时,会将访问节点移动到链表末尾。实现方式是,先连接起当前节点的before和after,再将当前节点连接到原tail后面。
基于LinkedHashMap实现LRU缓存时,还可通过覆写removeEldestEntry
(移除最老节点的策略,比如设定个最大链表大小,超过这个大小就返回true,移除最老节点)方法,实现自定义策略的LRU缓存。
继承自 HashMap,具有和 HashMap 一样的快速查找特性。
内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。
1 2 3 4 5 6 7 8 transient LinkedHashMap.Entry<K,V> head;transient LinkedHashMap.Entry<K,V> tail;
accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序。
1 final boolean accessOrder;
LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。
afterNodeAccess() 当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void afterNodeAccess (Node<K,V> e) { LinkedHashMap.Entry<K,V> last; 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; } }
afterNodeInsertion() 在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。 evict 只有在构建 Map 的时候才为 false,在这里为 true。
1 2 3 4 5 6 7 void afterNodeInsertion (boolean evict) { LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null , false , true ); } }
每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。
WeekHashMap