zoukankan      html  css  js  c++  java
  • WeakHashMap源码解读

    1. 简介

    本文基于JDK8u111的源码分析WeakHashMap的一些主要方法的实现。

    2. 数据结构

    就数据结构来说WeakHashMap与HashMap原理差不多,都是拉链法来解决哈希冲突。
    下面是WeakHashMap中的Entry结构定义。

    /**
      * 省略部分方法实现。
      */
    private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;
    
        Entry(Object key, V value,
                ReferenceQueue<Object> queue,
                int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
        @SuppressWarnings("unchecked")
        public K getKey() {
            return (K) WeakHashMap.unmaskNull(get());
        }
        public V getValue() {
            return value;
        }
    
        public V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
    
    }
    

    另外,每个WeakHashMap内部都有一个ReferenceQueue用于收集被GC的弱引用,定义如下。

    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
    

    这个queue会作为Entry构造方法的一个参数用于实例化WeakReference,其主要作用是为方便清理WeakHashMap中无效Entry。

    3. 重要方法源码

    3.1 哈希算法

    首先看一下WeakHashMap是如何去hash一个Object的

    final int hash(Object k) {
        int h = k.hashCode();
    
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    
    

    可以看到WeakHashMap的hash方法实际上是和JDK7中HashMap是相同的。
    因为WeakHashMap与HashMap类似,Capacity也是2的幂。如果直接用对象的hashCode那么在计算bucket的index的时候可能会出现比较严重的冲突(高位不同,低位相同分配到同一个bucket中)。为了避免这种情况,需要将高位与低位作一个混淆或者扰动,增加bucket index的随机性。

    在JDK8的HashMap类中,hash方法已经简化为只需要一次扰动亦或。

    3.2 插入元素

    public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
    
        // getTable会作一次清理。
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);
    
        // 遍历bucket中元素,查询是否命中map中已有元素。
        for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
            if (h == e.hash && eq(k, e.get())) {
                V oldValue = e.value;
                if (value != oldValue)
                    e.value = value;
                return oldValue;
            }
        }
    
        modCount++;
        Entry<K,V> e = tab[i];
        // 将新元素插入到bucket中。
        tab[i] = new Entry<>(k, value, queue, h, e);
    
        // 超过阈值后扩容一倍。
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }
    private Entry<K,V>[] getTable() {
        expungeStaleEntries();
        return table;
    }
    

    下面来看看WeakHashMap是如何清理脏数据的

    private void expungeStaleEntries() {
        // 遍历该WeakHashMap的reference queue中被回收的弱引用。
        for (Object x; (x = queue.poll()) != null; ) {
            /*
             * 这里有个值得注意的点就是下面的代码被包在queue的同步块中。
             * 因为这里不同步的话,WeakHashMap在不涉及修改,只有并发读的情况下,
             * 下面的清理在多线程情况下可能会破坏内部数据结构。
             * 而之所以不在整个方法级别作同步,原因是上面的ReferenceQueue的poll方法是线程安全,
             * 可以并发取数据的(poll方法里面有同步)。
             */
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);
    
                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                // 遍历对应bucket中的元素。
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        // 意味着table[i]==e,直接将table[i]向后指一位即可
                        if (prev == e)
                            table[i] = next;
                        else
                            // 删除p节点,将前驱和后继链接上。
                            prev.next = next;
                        // 因为可能有HashIterator正在遍历,所以e.next这里不清为null。
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }
    

    3.2.1 扩容机制

    与HashMap类似,在WeakHashMap中元素超过阈值threshold时也会发生扩容,下面是WeakHashMap的resize方法实现

    void resize(int newCapacity) {
        Entry<K,V>[] oldTable = getTable();
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
    
        Entry<K,V>[] newTable = newTable(newCapacity);
        transfer(oldTable, newTable);
        table = newTable;
    
        /*
         * WeakHashMap这里有个很严谨的设计是会再次判断元素个数是否超过阈值的一半
         * 因为在刚开始getTable以及后续transfer过程中都有清理机制(transfer方法不会去拷贝已经被回收的元素)。
         * 如果size的值小于阈值的一半,为了避免WeakHashMap的Capacity的无限扩张,会去重新将元素拷贝到原先的数组中。
         */
        if (size >= threshold / 2) {
            threshold = (int)(newCapacity * loadFactor);
        } else {
            expungeStaleEntries();
            transfer(newTable, oldTable);
            table = oldTable;
        }
    }
    
    
    private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
        for (int j = 0; j < src.length; ++j) {
            Entry<K,V> e = src[j];
            src[j] = null;
            while (e != null) {
                Entry<K,V> next = e.next;
                Object key = e.get();
                if (key == null) {
                    e.next = null; 
                    e.value = null; 
                    size--;
                } else {
                    int i = indexFor(e.hash, dest.length);
                    e.next = dest[i];
                    dest[i] = e;
                }
                e = next;
            }
        }
    }
    

    至此,put方法我们已经阅读的差不多了。这里梳理一下WeakHashMap在我们操作put元素时哪些情况下会清理元素

    • put方法开始的getTable会调用一次expungeStaleEntries
    • 需要扩容时resize方法开始的getTable会调用一次expungeStaleEntries
    • transfer方法本身会判断弱引用指向的对象是否已经被GC
    • 扩容后发现size小于阈值一半,会调用一次expungeStaleEntries

    3.3 取出元素

    WeakHashMap根据key获取一个mapping对应的value还是相对比较简单的。

    public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        // 遍历bucket中元素。
        while (e != null) {
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
    }
    

    可以看到在get方法中也有getTable方法的调用,这里也会涉及到已被GC的key对应entry的清理。

    3.3 删除元素

    public V remove(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);
        Entry<K,V> prev = tab[i];
        Entry<K,V> e = prev;
    
        while (e != null) {
            Entry<K,V> next = e.next;
            /* 
             * 这里的逻辑其实和expungeStaleEntries类似,
             * 如果在bucket最外的端点,则直接把tab[i]的指向往后面挪一下即可,
             * 否则将待删除节点前驱和后继链接上即可。
             */
            if (h == e.hash && eq(k, e.get())) {
                modCount++;
                size--;
                if (prev == e)
                    tab[i] = next;
                else
                    prev.next = next;
                return e.value;
            }
            prev = e;
            e = next;
        }
    
        return null;
    }
    

    4. WeakHashMap的使用

    WeakHashMap的一种使用场景是不影响key生命周期的缓存。可以参考tomcat中的ConcurrentCache中,使用了WeakHashMap。我们来看下代码:

    public final class ConcurrentCache<K,V> {
    
        private final int size;
    
        private final Map<K,V> eden;
    
        private final Map<K,V> longterm;
    
        public ConcurrentCache(int size) {
            this.size = size;
            this.eden = new ConcurrentHashMap<>(size);
            this.longterm = new WeakHashMap<>(size);
        }
    
        public V get(K k) {
            V v = this.eden.get(k);
            if (v == null) {
                synchronized (longterm) {
                    v = this.longterm.get(k);
                }
                if (v != null) {
                    this.eden.put(k, v);
                }
            }
            return v;
        }
    
        public void put(K k, V v) {
            if (this.eden.size() >= size) {
                synchronized (longterm) {
                    this.longterm.putAll(this.eden);
                }
                this.eden.clear();
            }
            this.eden.put(k, v);
        }
    }
    

    不过在我实际项目开发中,一般碰到需要用到WeakHashMap的场景还是比较少见的。

    5. 总结

    下面总结一下WeakHashMap,并和HashMap以及ThreadLocalMap作一个比较。

    比较内容 WeakHashMap HashMap ThreadLocalMap
    存储方式 拉链法 拉链法 开放地址法
    存储数据 任意,key/value可为null,实际存储的Entry为key的弱引用。 任意,key/value可为null key为ThreadLocal对象,value任意类型可为null,key一定不会为null(没法自己塞null),实际存储的Entry是key的弱引用。
    对key的GC影响 Entry为弱引用,不影响key的GC 强引用,对key的GC有影响 Entry为弱引用,不影响key的GC
    线程安全
    其它 自带无效数据清理 JDK8中方法实现有优化 自带无效数据清理
  • 相关阅读:
    13 款开源的全文检索引擎
    Laravel5.5 Jwt 1.0 beta 配置
    Laravel SQL 查询语句集锦
    laravel在中间件内生成的变量如何传到控制器
    laravel中的自定义函数的加载和第三方扩展库加载
    laravel5.5 dingo/api+jwt-auth
    微信小程序之使用checkbox
    微信小程序之使用wx:for遍历循环
    微信小程序之页面导航栏
    微信小程序之数据缓存
  • 原文地址:https://www.cnblogs.com/micrari/p/8443170.html
Copyright © 2011-2022 走看看