JDK 12 LinkedHashMap 源码分析

LinkedHashMap 是什么,能做什么,这里就不再展开讲了。这篇博客 有相关介绍,并展示了 LinkedHashMap 的核心原理,但是我发现我的 jdk 里的源代码和博主提供的源代码示例不一致,我的是 "12.0.1" 2019-04-16,所以就写了这篇文章,看看新版本的有哪些调整,以及为什么有这些调整。

1. 类注释

在类注释中,总结一下大致有以下几个要点:

  • 与 HashMap 不同,LinkedHashMap 维护了一个双向链表来定义迭代顺序
  • re-insert 一个已有的元素不会改变迭代顺序
  • 迭代顺序默认按插入顺序,但是也可以初始化为按访问顺序,这样就很适合用来实现 LRU cache
  • 在 LinkedHashMap 上迭代的时间复杂度是 O(size),而在 HashMap 上迭代是 O(capacity). 其他操作基本上都是 O(1),但因为维护双向链表的原因,性能上稍微逊于 HashMap
  • LinkedHashMap 的性能受 initial capacityload factor 的影响,这两个参数是从 HashMap 继承下来的,所以 HashMap 也是;但是因为迭代策略的不同,initial capacity 的值很大,不会直接影响迭代性能
  • 线程不安全。因为继承自 HashMap,许多性质也一样
  • add/delete 会影响到迭代顺序;插入顺序下,改变 pair 的 value 不会影响;访问顺序下,get 操作也会影响到迭代顺序。这个根据定义很好理解
  • 迭代过程中如果被其他线程修改,会以 fail-fast 的策略尽快抛出 ConcurrentModificationException,需要注意的是,这个也只是 best-effort 的行为,并不能保证在冲突的第一时间就抛出异常,所以捕获异常后的 map 是不确定的
  • 应该是 JDK8 新增的并行部分,暂时没看 The spliterators returned by the spliterator method of the collections returned by all of this class's collection view methods are late-binding,fail-fast, and additionally report {@link Spliterator#ORDERED}.

2. 继承结构

LinkedHashMap 继承自 HashMap,重用了部分属性,重写了部分方法;自己额外定义的主要包括一些内部类、构造函数等。


LinkedHashMap 的结构

3. 从一个 demo 开始

public class Test {

    public static void main(String[] args) {
        LinkedHashMap map = new LinkedHashMap<>();
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);

        HashMap hashMap = new HashMap<>();
        hashMap.put("111", "111");
        hashMap.put("333", "333");
        hashMap.put("222", "222");
        System.out.println(hashMap);
    }
}

// output
{111=111, 333=333, 222=222}
{111=111, 222=222, 333=333}

println 方法对 object 类型的参数,会调用 object 的 toString() 方法;map 系列的这个方法是定义在 AbstractMap 里面的,拿到 entrySet 的 iterator,再通过 iterator 的 next 方法来迭代。

// AbstractMap.java
public String toString() {
        Iterator> i = entrySet().iterator();
        //...
        for (;;) {
            Entry e = i.next();
        //...
        }
}

// AbstractMap.java
public abstract Set> entrySet();

而 LinkedHashMap 和 HashMap 的 entrySet() 方法也不相同,

// LinkedHashMap.java
public Set> entrySet() {
        Set> es;
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}

// HashMap.java
public Set> entrySet() {
        Set> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

所以主要是 LinkedEntrySet 和 EntrySet 导致的区别,下面列出了有区别的方法,可以发现实际上又是 Iterator 导致的区别

  final class EntrySet extends AbstractSet> {
        public final Iterator> iterator() {
            return new EntryIterator();
        }
        public final Spliterator> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer> action) {
            Node[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (Node e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
  }

  final class LinkedEntrySet extends AbstractSet> {
        public final Iterator> iterator() {
            return new LinkedEntryIterator();
        }
        public final Spliterator> spliterator() {
            return Spliterators.spliterator(this, Spliterator.SIZED |
                                            Spliterator.ORDERED |
                                            Spliterator.DISTINCT);
        }
        public final void forEach(Consumer> action) {
            if (action == null)
                throw new NullPointerException();
            int mc = modCount;
            for (LinkedHashMap.Entry e = head; e != null; e = e.after)
                action.accept(e);
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

两者的 iterator 的 next()方法都是调用了

public final Map.Entry next() { return nextNode(); }

首先注意,nextNode() 返回的是初始化/上次计算 的 next 值,并计算出下一个值。nextNode 在 LinkedHashMap 和 HashMap 中有者不同的实现,

先来看看 HashIterator
next 由 HashIterator 构造函数初始化,并在 nextNode 方法中更新。table 是 hash bucket,一个数组。
首先是一个 fast-fail 地检测是否被并发 add/delete 了,(这个机制请参考其他博客,这里不再赘述),然后把指针在 table 上后移,如果 next 不为空则直接返回;如果为空,则要跳过一个槽看下一个,循环;

所以,HashMap 的迭代复杂度是 O(capacity),因为它需要检查 table 上的每一个元素

  abstract class HashIterator {
        Node next;        // next entry to return
        Node current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
        
        HashIterator() {
            expectedModCount = modCount;
            Node[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        final Node nextNode() {
            Node[] t;
            Node e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
    }

再来看看 LinkedHashIterator
一样的 fail-fast check,但是神奇的地方出现了,next = e.after,完事儿,完全不跟你多bb。可以肯定,这个 after 指向的肯定是按 insert order/access order 的下一个元素。那么这个 after 又是哪里冒出来的呢?

  abstract class LinkedHashIterator {
        final LinkedHashMap.Entry nextNode() {
            LinkedHashMap.Entry e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }
    }

所以,问题的核心还是回到了 LinkedHashMap.Entry 上。

在 Collections 框架里,Entry 应该是接口,用于定义键值对;实体类应该是 XXXNode 才对。对于这一点,源代码中的注释也给出了说明:HashMap now uses trees for some of its nodes, class LinkedHashMap.Entry is now treated as intermediary node class that can also be converted to tree form. The name of this class, LinkedHashMap.Entry, is confusing in several ways in its current context, but cannot be changed. but cannot be changed Otherwise, even though it is not exported outside this package, some existing source code is known to have relied on a symbol resolution corner case rule in calls to removeEldestEntry that suppressed compilation errors due to ambiguous usages. So, we keep the name to preserve unmodified compilability.
也就是说,一开始没有考虑到规范性的问题,而 HashMap 又用了 LinkedHashMap.Entry 来实现 TreeNode;即使这个静态内部类没有暴露出去,但是有的程序,是通过名字来解析这个类的,如果改了名字会导致编译都过不了,所以为了兼容就不改了。

HashMap 中定义了 Node,LinkedHashMap.Entry 继承自 Node。多了两个属性变量,before 和 after。根据名字我们可以猜到,这是一个双向链表的元素。源代码如下,但是初始化一个 Entry 的时候并没有设置 before 和 after 信息,那么双向链表的维护必定是在 Map 的操作过程中。

  /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
  static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;

        Node(int hash, K key, V value, Node next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }   
  }

  /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
  static class Entry extends HashMap.Node {
        Entry before, after;
        Entry(int hash, K key, V value, Node next) {
            super(hash, key, value, next);
        }
  }

经过上述分析,我们已经清楚 LinkedHashMap 的大致结构和原理。下面我们来具体看看这个双向链表是怎么维护的。
回到我们一开始的程序

public class Test {

    public static void main(String[] args) {
        LinkedHashMap map = new LinkedHashMap<>();
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);
    }
}

// output
{111=111, 333=333, 222=222}

检查 LinkedHashMap 的构造函数,accessOrder 被设置为 false.

public LinkedHashMap() {
        super();
        accessOrder = false;
}

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * 标识 LinkedHashMap 的迭代顺序: {@code true}
 * for access-order, {@code false} for insertion-order.
 */
final boolean accessOrder;

再看 put 方法,直接就进入了 HashMap 里面。奇怪了对吧?没有重写 put 方法。那是在哪里设置的 before/after 呢?
PS:putVal 方法比较复杂,是个核心算法,可以研究下。

  public V put(K key, V value) {
      return putVal(hash(key), key, value, false, true);
  }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node 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)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashMap 的 putVal 方法调用了 newNode(),afterNodeAccess() 等。在 HashMap 的源代码中可以看见如下注释:

    /* ------------------------------------------------------------ */
    // LinkedHashMap support


    /*
     * The following package-protected methods are designed to be
     * overridden by LinkedHashMap, but not by any other subclass.
     * Nearly all other internal methods are also package-protected
     * but are declared final, so can be used by LinkedHashMap, view
     * classes, and HashSet.
     */

    // Create a regular (non-tree) node
    Node newNode(int hash, K key, V value, Node next) {
        return new Node<>(hash, key, value, next);
    }
    ...

而在 LinkedHashMap 中,这些方法都被重写了


LinkedHashMap 重写父类中的方法

以 newNode 为例,初始化一个 entry 后,调用 linkNodeLast 来维护 before/after 指针。到这里,我们终于知道为什么 LinkedHashMap 有顺序了。LinkedHashMap 也需要在其他方法里补上对 before/after 的操作,这里不再逐一分析。

  Node newNode(int hash, K key, V value, Node e) {
        LinkedHashMap.Entry p =
            new LinkedHashMap.Entry<>(hash, key, value, e);
        linkNodeLast(p);
        return p;
  }

// internal utilities

// link at the end of list
  private void linkNodeLast(LinkedHashMap.Entry p) {
        LinkedHashMap.Entry last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
  }

还有一件事,那就是如何通过 accessOrder 来区分 insert order/access order的。默认是 insert order,不需要做额外的操作;而 access order,则需要在每次访问 entry 后,调整 entry 的位置。HashMap 的设计者暴露出了一个 afterNodeAccess 回调,以 LinkedHashMap#get(K) 方法为例,如果是 access order,会执行 afterNodeAccess(e)

    public V get(Object key) {
        Node e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

在 LinkedHashMap.afterNodeAccess 中,会判断是否是 accessOrder,是的话把这个 entry 放到双向链表的最后。至于为什么是最后,正常人应该是如 LRU cache 一样放到最前,别问,问就是最后。

  void afterNodeAccess(Node e) { // move node to last
        LinkedHashMap.Entry last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry p =
                (LinkedHashMap.Entry)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;
        }
  }

所以,LinkedHashMap 还提供了一个 removeEldestEntry 回调,默认为 false,子类可以重写来实现当是否删除最 eldest 的 entry。这个回调会在 put/putAll 方法时触发,何为 eldest 呢?对于 insert order,eldest 就是 put/putAll 加入的(最后一个)元素;对于 access order,eldest 就是 head 指针指向的元素(对应前面的移到最后)。

  /**
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns {@code true}.  If the map was empty prior
     *           to the {@code put} or {@code putAll} invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     * @return   {@code true} if the eldest entry should be removed
     *           from the map; {@code false} if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return false;
    }

这种回调设计还有好几个,下面是一些定义在 HashMap 中的回调

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node p) { }

下面再看一下 access order 的验证 demo,因为 "333" 被 "最近访问" 了,所以他被移到了链表的最后。

    public static void main(String[] args) {
        LinkedHashMap map = new LinkedHashMap<>(16, 0.75f, true);
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);

        map.get("333");
        System.out.println(map);
    }

// output
{111=111, 333=333, 222=222}
{111=111, 222=222, 333=333}

4. 如何实现 LRU cache

这是 leetcode 上的一个实现,思路很明显了

class LRUCache extends LinkedHashMap {
    private int capacity;
    public LRUCache(int capacity) {
        super(capacity, 0.8F, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        super.put(key, value);
    }
    
    @Override
    public boolean removeEldestEntry(Map.Entry entry) {
        return size() > capacity;
    }
}

你可能感兴趣的:(JDK 12 LinkedHashMap 源码分析)