LinkedHashMap分析
这篇文章会分析一下 LinkedHashMap。
LinkedHashMap是啥
首先我们看一下这个类的定义:
1 public class LinkedHashMap<K,V> 2 extends HashMap<K,V> 3 implements Map<K,V>
可以看到LinkedHashMap同样实现了Map接口,但它同时还继承了HashMap,所以天然就有HashMap自身的特性。
从名字上看 LinkedHashMap 比 HashMap 多了前面的“ Linked “, 那它们之间的区别在哪里呢?我们再看一段代码:
1 /** 2 * LinkedHashMap的Entry对象,除了继承于HashMap Node的4个属性,额外添加了两个属性before,after用于维护双向链表的前后结点关系 3 */ 4 static class Entry<K,V> extends HashMap.Node<K,V> { 5 Entry<K,V> before, after; 6 Entry(int hash, K key, V value, Node<K,V> next) { 7 super(hash, key, value, next); 8 } 9 }
从这里可以看到相比较于HashMap的Node,除了继承自HashMap Node数据结构中的hash、key、value、next四个属性之外,LinkedHashMap的Node还多维护了两个属性:before,after。
这两个属性是用于维护一条独立的双向链表,而这个双向链表中保存的仅仅是在LinkedHashMap中节点与节点的前后关系。
所以可以这么认为:LinkedHashMap是 "维护了节点之间前后顺序的HashMap" 。
其他用到的属性:
1 /** 2 * 双向链表的头结点(最近最少使用的结点) 3 */ 4 transient LinkedHashMap.Entry<K,V> head; 5 6 /** 7 * 双向链表的尾结点(最近最多使用的结点) 8 */ 9 transient LinkedHashMap.Entry<K,V> tail; 10 11 /** 12 * LinkedHashMap核心属性,该属性定义了双向链表中存储结点的顺序: 13 * 如果为true,则双向链表按照结点的访问顺序维护前后结点关系(访问、操作结点都会影响该结点在双向链表的位置),这种方式实现了LRU算法 14 * 如果为false,则双向链表仅仅按照结点的插入顺序维护前后结点关系(只有操作结点的动作才会影响该结点在双向链表的位置) 15 * 该值默认为false. 16 */ 17 final boolean accessOrder;
这里需要着重注意第17行的accessOrder这个变量,LinkedHashMap通过在构造方法中传入该参数的值(true/false)来控制双向链表中维护节点的前后顺序时是根据访问顺序维护还是插入顺序维护。解释一下这两种顺序有啥区别:
假设一开始往Map中添加了3个节点 A、B、C,则此时双向链表中维护节点的前后顺序应该是 A -> B -> C,此时调用HashMap的get(key)方法访问B节点:
如果 accessOrder = true, 那么按照访问节点维护双向链表中节点前后顺序,此时节点的前后顺序关系变更为 A -> C -> B;
如果accessOrder = false,那么节点的前后顺序关系不变,依旧为 A -> B -> C。
当然,如果是插入、删除节点,无论accessOrder设置为何值,双向链表中节点的前后顺序关系都会发生改变。比如在Map插入一个节点D
那么无论accessOrder为true还是false,双向链表中节点的前后顺序关系都会变更为 A -> B -> C -> D。
LinkedHashMap核心源码分析(JDK Version 1.8)
1.构造方法
1 /** 2 * 带capacity和loadFactor的构造函数 3 * 4 * @param initialCapacity 自定义初始容量 5 * @param loadFactor 自定义负载因子 6 * @throws IllegalArgumentException 初始容量或负载因子为负数,抛出异常 7 * 8 */ 9 public LinkedHashMap(int initialCapacity, float loadFactor) { 10 super(initialCapacity, loadFactor); //调用HashMap的构造函数 11 accessOrder = false; 12 } 13 14 /** 15 * 只带capacity参数的构造函数,此时loadFactor为默认的0.75 16 * 17 * @param initialCapacity 自定义初始容量 18 * @throws IllegalArgumentException 初始容量为负数,抛出异常 19 */ 20 public LinkedHashMap(int initialCapacity) { 21 super(initialCapacity); //调用HashMap的构造函数 22 accessOrder = false; 23 } 24 25 /** 26 * 无参数构造函数。初始容量为默认的16,负载因子为默认的0.75 27 */ 28 public LinkedHashMap() { 29 super(); //调用HashMap的构造函数 30 accessOrder = false; 31 } 32 33 34 /** 35 * 三个参数的构造函数,可以控制双向链表的存储方式 36 * 37 * @param initialCapacity 初始容量 38 * @param loadFactor 负载因子 39 * @param accessOrder 双向链表维护的存储顺序 40 * @throws IllegalArgumentException 初始容量或负载因子为负数,抛出异常 41 */ 42 public LinkedHashMap(int initialCapacity, 43 float loadFactor, 44 boolean accessOrder) { 45 super(initialCapacity, loadFactor); //调用HashMap的构造方法 46 this.accessOrder = accessOrder; 47 }
2.LinkedHashMap在访问和操作节点时特有的方法
1 /** 2 * LinkedHashMap重写了HashMap的newNode()方法 3 * 该方法在HashMap的putVal()方法中判断需要新增结点时会被调用 4 * @param hash 5 * @param key 6 * @param value 7 * @param e 8 * @return 9 */ 10 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { 11 LinkedHashMap.Entry<K,V> p = 12 new LinkedHashMap.Entry<K,V>(hash, key, value, e); //新增一个key-value的结点 13 linkNodeLast(p); //LinkedHashMap生成新结点时除了新增一个结点外,还将该结点在双向链表中的位置移动到尾部,这个操作默认按元素插入顺序维护了链表前后结点关系 14 return p; 15 } 16 17 18 /** 19 * 将结点在双向链表中的位置移动到尾部 20 * @param p 21 */ 22 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { 23 LinkedHashMap.Entry<K,V> last = tail; //将当前双向链表的尾结点保存下来 24 tail = p; //尾结点设置为传入的结点 25 if (last == null) //如果之前链表中没有结点,即这次新增的结点是链表的头结点 26 head = p; 27 else { //如果这次新增的结点不是链表的头结点,则将其移动到链表的尾部 28 p.before = last; //当前结点的前驱指向之前链表的尾结点 29 last.after = p; //之前链表尾结点后驱指向当前结点 30 } 31 } 32 33 34 /** 35 * 某个结点被删除后的回调方法 36 * LinkedHashMap在维护的双向链表中也要把该结点与前后结点的关系移除 37 * @param e 38 */ 39 void afterNodeRemoval(Node<K,V> e) { // unlink 40 LinkedHashMap.Entry<K,V> p = 41 (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; 42 p.before = p.after = null; //移除被删除结点和前后结点之前的引用关系 43 if (b == null) //如果双向链表中被移除的结点就是首结点 44 head = a; //将下一个结点变为首结点 45 else 46 b.after = a; //否则就把被删除结点的前一个结点的后驱指向被删除结点的后一个结点 47 if (a == null) //如果双向链表中被移除的结点就是尾结点 48 tail = b; //将上一个结点变为尾结点 49 else 50 a.before = b; //否则就把被删除结点的后一个结点的前驱指向被删除结点的前一个结点 51 } 52 53 54 /** 55 * 某个结点被访问后的回调方法 56 * 如果在创建LinkedHashMap时构造方法中的accessOrder设置为true 57 * 则双向链表需要按照元素访问的顺序维护双向链表中结点的前后引用关系以实现LRU算法 58 * 在每次get/put结点后,都需要调用这个回调方法,将结点在双向链表中的位置移动到尾部(因为最近这个结点被访问了) 59 * @param e 60 */ 61 void afterNodeAccess(Node<K,V> e) { // move node to last 62 LinkedHashMap.Entry<K,V> last; 63 if (accessOrder && (last = tail) != e) { //如果指定accessOrder为true并且当前访问的结点在双向链表中不是尾结点才继续做如下操作 64 LinkedHashMap.Entry<K,V> p = 65 (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; 66 p.after = null; //将当前被访问结点在双向链表中的后驱引用设置为null 67 if (b == null) //如果原先被访问结点的前驱引用为null,说明这个被访问结点原先就是双向链表的头结点 68 head = a; //将头结点变更为被访问结点的后一个结点 69 else 70 b.after = a; //否则将被访问结点的前一个结点的后驱指向被访问结点的后一个结点 71 if (a != null) //如果原先被访问结点在双向链表中有下一个结点 72 a.before = b; //那么将下一个结点的前驱引用指向被访问结点的前一个结点 73 else 74 last = b; //否则将尾结点引用指向设置为当前被访问结点的前一个结点 75 if (last == null) //如果原先双向链表中没有尾结点,则说明此次访问的结点是该双向链表中第一个结点 76 head = p; //将双向头结点设置为此次被访问的结点 77 else { //将被访问的结点移动到双向链表的尾部 78 p.before = last; //否则将当前结点的前驱引用指向原来双向链表的尾结点 79 last.after = p; //再将原先双向链表尾结点的后驱引用指向当前被访问的结点 80 } 81 tail = p; //双向链表尾结点变更为当前被访问的结点 82 ++modCount; 83 } 84 }
因为LinkedHashMap维护了双向链表,所以相比HashMap在访问、插入、删除节点时都要额外再对双向链表中维护的节点前后关系进行操作。
结合注释看应该很好理解,就不再啰嗦了。
LinkedHashMap的LRU特性
先讲一下LRU的定义:LRU(Least Recently Used),即最近最少使用算法,最初是用于内存管理中将无效的内存块腾出而用于加载数据以提高内存使用效率而发明的算法。
目前已经普遍用于提高缓存的命中率,如Redis、Memcached中都有使用。
为啥说LinkedHashMap本身就实现了LRU算法?原因就在于它额外维护的双向链表中。
在上面已经提到过,在做get/put操作时,LinkedHashMap会将当前访问/插入的节点移动到链表尾部,所以此时链表头部的那个节点就是 "最近最少未被访问"的节点。
举个例子:
往一个空的LinkedHashMap中插入A、B、C三个结点,那么链表会经历以下三个状态:
1. A 插入A节点,此时整个链表只有这一个节点,既是头节点也是尾节点
2. A -> B 插入B节点后,此时A为头节点,B为尾节点,而最近最常访问的节点就是B节点(刚被插入),而最近最少使用的节点就是A节点(相对B节点来讲,A节点已经有一段时间没有被访问过)
3. A -> B -> C 插入C节点后,此时A为头节点,C为尾节点,而最近最常访问的节点就是C节点(刚被插入),最近最少使用的节点就是A节点 (应该很好理解了吧 : ))
那么对于缓存来讲,A就是我最长时间没访问过的缓存,C就是最近才访问过的缓存,所以当缓存队列满时就会从头开始替换新的缓存值进去,从而保证缓存队列中的缓存尽可能是最近一段时间访问过的缓存,提高缓存命中率。
OK,到这里LinkedHashMap就分析完了,相比于HashMap还是比较容易理解的吧 : )
下一篇文章会分析 TreeMap 的具体实现。