WeakHashMap源码解析

news2024/11/19 22:44:30

WeakHashMap源码解析

简介

WeakHashMap 是一种 弱引用 map,内部的 key 会存储为弱引用,当 jvm gc 的时候,如果这些 key 没有强引用存在的话,会被 gc 回收掉,下一次当我们操作 map 的时候会把对应的 Entry 整个删除掉,基于这种特性,WeakHashMap 特别适用于 缓存 处理。

继承体系

image-20221204180736143

可见,WeakHashMap 没有实现 Clone 和 Serializable 接口,所以不具有克隆和序列化的特性。

存储结构

WeakHashMap 因为 gc 的时候会把没有强引用的 key 回收掉,所以注定了它里面的元素不会太多,因此也就不需要像 HashMap 那样元素多的时候转化为红黑树来处理了。

因此,WeakHashMap 的存储结构只有 (数组 + 链表)

源码解析

属性

    /*
     * 默认初始容量为16
     */
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    /*
     * 最大容量为2的30次方
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /*
     * 默认装载因子
     */
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /*
     * 桶
     */
    Entry<K,V>[] table;

    /*
     * 元素个数
     */
    private int size;

    /*
     * 扩容门槛,等于capacity * loadFactor
     */
    private int threshold;

    /*
     * 装载因子
     */
    private final float loadFactor;

    /*
     * 引用队列,当弱键失效的时候会把Entry添加到这个队列中
     */
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

(1)容量

  • 容量为数组的长度,亦即桶的个数,默认为 16,最大为 2 的 30 次方,当容量达到 64 时才可以树化。

(2)装载因子

  • 装载因子用来计算容量达到多少时才进行扩容,默认装载因子为 0.75。

(3)引用队列

  • 当弱键失效的时候会把 Entry 添加到这个队列中,当下次访问 map 的时候会把失效的 Entry 清除掉。

内部类Entry

WeakHashMap 内部的存储节点,没有 key 属性。

    // 继承与弱引用类WeakReference
	private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        // 可以发现没有key,因为key是作为弱引用存到Reference类中
        V value;
        final int hash;
        Entry<K,V> next;

        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            // 调用WeakReference的构造方法初始化key和引用队列
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
    }

    public class WeakReference<T> extends Reference<T> {
        public WeakReference(T referent, ReferenceQueue<? super T> q) {
            // 调用Reference的构造方法初始化key和引用队列
            super(referent, q);
        }
    }

    public abstract class Reference<T> {
        // 实际存储key的地方
        private T referent;         /* Treated specially by GC */
        // 引用队列
        volatile ReferenceQueue<? super T> queue;

        Reference(T referent, ReferenceQueue<? super T> queue) {
            this.referent = referent;
            this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
        }
    }

从 Entry 的构造方法我们知道,key 和 queue 最终会传到到 Reference 的构造方法中,这里的 key 就是 Reference 的 referent 属性,它会被 gc 特殊对待,即当没有强引用存在时,当下一次 gc 的时候会被清除

构造方法

    /*
     * 下面三个构造方法都是调用的这个构造方法
     * @param initialCapacity:初始容量
     * @param loadFactor:初始负载因子
     */
	public WeakHashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Initial Capacity: "+
                    initialCapacity);
        // 如果初始容量超过了最大容量,则赋值为最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load factor: "+
                    loadFactor);
        // capacity赋值为1
        int capacity = 1;
        // 如果capacity小于传进来的initialCapacity
        // while一直循环,直到出现一个 2的n次方的值 大于传入容量,则就是初始容量
        while (capacity < initialCapacity)
            // 将capacity左移一位,也就是capacity = capacity * 2
            capacity <<= 1;
        // 创建一个容量为 大于等于传入容量最近的2的n次方的Entry数组
        table = newTable(capacity);
        this.loadFactor = loadFactor;
        // 扩容阈值
        threshold = (int)(capacity * loadFactor);
    }

    public WeakHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public WeakHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public WeakHashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY),
                DEFAULT_LOAD_FACTOR);
        putAll(m);
    }

构造方法与 HashMap 基本类似,初始容量为大于等于传入容量最近的 2 的 n 次方,扩容门槛 threshold 等于 capacity * loadFactor。

put(K key, V value)

  • 添加元素的方法。
    public V put(K key, V value) {
        // 如果key为空,用空对象代替
        Object k = maskNull(key);
        // 计算key的hash值
        int h = hash(k);
        // 获取桶
        Entry<K,V>[] tab = getTable();
        // 计算元素在哪个桶中,h & (length-1)
        int i = indexFor(h, tab.length);

        // 遍历桶对应的链表
        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];
        tab[i] = new Entry<>(k, value, queue, h, e);
        // 如果插入元素后数量达到了扩容门槛就把桶的数量扩容为2倍大小
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }

(1)计算 hash;

  • 这里与 HashMap 有所不同,HashMap 中如果 key 为空直接返回 0,这里是用空对象来计算的。

  • 另外打散方式也不同,HashMap 只用了一次异或,这里用了四次,HashMap 给出的解释是一次够了,而且就算冲突了也会转换成红黑树,对效率没什么影响。

(2)计算在哪个桶中;

(3)遍历桶对应的链表;

(4)如果找到元素就用新值替换旧值,并返回旧值;

(5)如果没找到就在链表头部插入新元素;

  • HashMap 就插入到链表尾部。

(6)如果元素数量达到了扩容门槛,就把容量扩大到 2 倍大小;

  • HashMap中是大于 threshold 才扩容,这里等于 threshold 就开始扩容了。

resize(int newCapacity)

  • 扩容方法。
    void resize(int newCapacity) {
        // 获取旧桶,getTable()的时候会剔除失效的Entry(key为null,被gc回收),因为调用了expungeStaleEntries()方法,下文会讲
        Entry<K,V>[] oldTable = getTable();
        // 旧容量
        int oldCapacity = oldTable.length;
        // 如果就容量已经等于table最大容量,则直接赋值为int最大值 并返回
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        // 新桶
        Entry<K,V>[] newTable = newTable(newCapacity);
        // 把元素从旧桶转移到新桶
        transfer(oldTable, newTable);
        // 把新桶赋值桶变量
        table = newTable;

        // 如果元素个数大于扩容门槛的一半,则使用新桶和新容量,并计算新的扩容门槛
        if (size >= threshold / 2) {
            threshold = (int)(newCapacity * loadFactor);
        } else {
            /*
             * 否则把元素再转移回旧桶,还是使用旧桶
             * 因为在transfer()的时候会清除失效的Entry,所以元素个数可能没有那么大了,就不需要扩容了
             */
            // 再次清理过期的Entry
            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];
            // 将当前桶位置为null
            src[j] = null;
            // 遍历该桶位的链表
            while (e != null) {
                Entry<K,V> next = e.next;
                // 获取当前元素的key
                Object key = e.get();
                // 如果key等于了null就清除,说明key被gc清理掉了,则把整个Entry清除
                if (key == null) {
                    e.next = null;  // Help GC
                    e.value = null; //  "   "
                    size--;
                } else {
                    // 否则就计算在新桶中的位置并把这个元素放在新桶对应链表的头部
                    int i = indexFor(e.hash, dest.length);
                    e.next = dest[i];
                    dest[i] = e;
                }
                e = next;
            }
        }
    }

(1)判断旧容量是否达到最大容量;

(2)新建新桶并把元素全部转移到新桶中;

(3)如果转移后元素个数不到扩容门槛的一半,则把元素再转移回旧桶,继续使用旧桶,说明不需要扩容;

(4)否则使用新桶,并计算新的扩容门槛;

(5)转移元素的过程中会把 key 为 null 的元素清除掉,所以 size 会变小;

get(Object key)

  • 获取元素。
    public V get(Object key) {
        // 如果key为空,用空对象代替
        Object k = maskNull(key);
        // 计算hash
        int h = hash(k);
        // 返回table,并做一次expungeStaleEntries()
        Entry<K,V>[] tab = getTable();
        // 计算下标
        int index = indexFor(h, tab.length);
        // 找到所在的桶
        Entry<K,V> e = tab[index];
        // 遍历链表,找到了就返回
        while (e != null) {
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        // 没找到则返回null
        return null;
    }

(1)计算 hash 值;

(2)遍历所在桶对应的链表;

(3)如果找到了就返回元素的 value 值;

(4)如果没找到就返回空;

remove(Object key)

  • 移除元素。
    public V remove(Object key) {
        // 如果key为空,用空对象代替
        Object k = maskNull(key);
        // 计算hash
        int h = hash(k);
        // 返回table,并做一次expungeStaleEntries()
        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;
            if (h == e.hash && eq(k, e.get())) {
                // 如果找到了就删除元素
                modCount++;
                size--;

                if (prev == e)
                    // 如果是头节点,就把头节点指向下一个节点
                    tab[i] = next;
                else
                    // 如果不是头节点,删除该节点
                    prev.next = next;
                // 返回该节点的value值
                return e.value;
            }
            // 向后迭代
            prev = e;
            e = next;
        }
		// 没找到则返回null
        return null;
    }

(1)计算 hash;

(2)找到所在的桶;

(3)遍历桶对应的链表;

(4)如果找到了就删除该节点,并返回该节点的 value 值;

(5)如果没找到就返回 null;

expungeStaleEntries()

  • 剔除失效的 Entry(key 为 null,被 gc 回收)。
    /*
     * 当key失效的时候gc会自动把对应的Entry添加到引用队列中
     * 此方法就是将引用队列中所有的Entry从map中剔除掉
     */
	private void expungeStaleEntries() {
        // 遍历引用队列
        for (Object x; (x = queue.poll()) != null; ) {
            // 加锁
            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;
                // 遍历链表
                while (p != null) {
                    Entry<K,V> next = p.next;
                    // 找到该元素
                    if (p == e) {
                        // 删除该元素
                        if (prev == e)
                            // 如果是头节点,就把头节点指向下一个节点
                            table[i] = next;
                        else
                            // 如果不是头节点,删除该节点
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        // 将value置为null
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    // 向后迭代
                    prev = p;
                    p = next;
                }
            }
        }
    }

(1)当 key 失效的时候 gc 会自动把对应的 Entry 添加到这个引用队列中;

(2)所有对 map 的操作都会直接或间接地调用到这个方法先移除失效的 Entry,比如 getTable()size()resize()

(3)这个方法的目的就是遍历引用队列,并把其中保存的 Entry 从 map 中移除掉,具体的过程请看类注释;

(4)从这里可以看到移除 Entry 的同时把 value 也一并置为 null 帮助 gc 清理元素,防御性编程。

使用案例

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapTest {
    
    public static void main(String[] args) {
        Map<String, Integer> map = new WeakHashMap<>(3);

        // 放入3个new String()声明的字符串
        map.put(new String("1"), 1);
        map.put(new String("2"), 2);
        map.put(new String("3"), 3);

        // 放入不用new String()声明的字符串
        map.put("6", 6);

        // 使用key强引用"3"这个字符串
        String key = null;
        for (String s : map.keySet()) {
            // 这个"3"和new String("3")不是一个引用
            if (s.equals("3")) {
                key = s;
            }
        }

        // 输出{6=6, 1=1, 2=2, 3=3},未gc所有key都可以打印出来
        System.out.println(map);

        // gc一下
        System.gc();

        // 放一个new String()声明的字符串
        map.put(new String("4"), 4);

        // 输出{4=4, 6=6, 3=3},gc后放入的值和强引用的key可以打印出来
        System.out.println(map);

        // key与"3"的引用断裂
        key = null;

        // gc一下
        System.gc();

        // 输出{6=6},gc后强引用的key可以打印出来
        System.out.println(map);
    }
}

在这里通过 new String() 声明的变量才是弱引用,使用 “6” 这种声明方式会一直存在于常量池中,不会被清理,所以 “6” 这个元素会一直在 map 里面,其它的元素随着 gc 都会被清理掉。

总结

(1)WeakHashMap 使用 (数组 + 链表) 存储结构;

  • 单纯作为 Map 没有 HashMap 好:HashMap 在 jdk8 做了很多优化,比如单链表在过长时会转化为红黑树,降低极端
    情况下的操作复杂度。但 WeakHashMap 没有相应的优化,有点像 jdk8 之前的 HashMap 版本。

(2)WeakHashMap 中的 key 是 弱引用,gc 的时候会被清除;

(3)每次对 map 的操作都会剔除失效 key 对应的 Entry;

(4)使用 String 作为 key 时,一定要使用 new String() 这样的方式声明 key,才会失效,其它的基本类型的包装类型是一样的;

(5)WeakHashMap 常用来作为 缓存 使用;




参考文章

  • 彤哥读源码_死磕 java集合之WeakHashMap源码分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/106677.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

金融信息科技服务外包风险管理能力成熟度评估规范 学习笔记 附录下载地址

金融信息科技服务外包风险管理的范围 本标准规定了金融业信息科技服务外包风险管理能力成熟度评估体系以及对发包方和承包方的总体要求&#xff0c;分别对发包方、承包方的服务外包风险管理能力成熟度进行了分级定义&#xff0c;并规定了对发包方和承包方进行服务外包风险管理…

《OpenGL 模型》 渲染出帅气的暗影战士

模型Assimp流程网格模型效果Assimp 3D建模工具&#xff0c;可以让艺术家创建复杂的形状&#xff0c;Assimp库用于加载&#xff0c;如加载obj格式的文件到我们的程序之中&#xff0c;下载CMAKE用于构建该库&#xff08;会有很多问题&#xff09;&#xff0c;不过&#xff01;我…

【小程序】小程序代码的构成

目录 项目结构 1. 了解项目的基本组成结构 2. 小程序页面的组成部分 JSON配置文件 1. JSON 配置文件的作用 2. app.json 文件 3. project.config.json 文件 4. sitemap.json 文件 5. 页面的 .json 配置文件 6. 新建小程序页面 7. 修改项目首页 项目结构 1. 了解项…

别再用过时的方式了!全新版本Spring Security,这样用才够优雅!

基本使用 我们先对比下Spring Security提供的基本功能登录认证&#xff0c;来看看新版用法是不是更好。 升级版本 首先修改项目的pom.xml文件&#xff0c;把Spring Boot版本升级至2.7.0版本。 <parent><groupId>org.springframework.boot</groupId><art…

Lua 元表(Metatable)

在 Lua table 中我们可以访问对应的 key 来得到 value 值&#xff0c;但是却无法对两个 table 进行操作(比如相加)。 因此 Lua 提供了元表(Metatable)&#xff0c;允许我们改变 table 的行为&#xff0c;每个行为关联了对应的元方法。 例如&#xff0c;使用元表我们可以定义 …

STM32程序设计规范浅析

这篇博客写到“STM32基础知识篇”里&#xff0c;一方面是一个很好地对过往工作的总结&#xff0c;另一方面也是整个专栏撰写计划的开端&#xff0c;古人云&#xff1a;良好的开端是成功的一半&#xff0c;在文章的最后详细地规划了整个专栏后期的更新计划。 笔者前段时间休息的…

无人机遥感图像拼接与处理操作技术

【内容简述】&#xff1a; 无人机遥感图像采集流程&#xff1a; 无人机遥感监测介绍 无人机航线规划设计 无人机飞行软件操作 无人机航拍一般过程 无人机遥感图像拼接软件操作&#xff1a; Photoscan软件介绍 软件基本操作与实践 遥感图像拼接的一般流程 遥感图像分组拼接与点…

【centos】安装nvida CUDA平台附带安装cudnn库

目录1.安装 CUDAToolKit2.安装cudnn库1.安装 CUDAToolKit 使用 lspci | grep -i nvidia列出所有支持的GPU 安装内核开发依赖包&#xff1a; yum install kernel-devel查看内核版本号&#xff0c;用来看与开发包版本号是否一致&#xff1a; uname -r查看nvida显卡驱动&#…

设计模式之迭代器模式

Iterator design pattern 迭代器模式的概念、迭代器模式的结构、迭代器模式的优缺点、迭代器模式的使用场景、迭代器模式的实现示例、迭代器模式的源码分析 1、迭代器模式的概念 迭代器模式&#xff0c;即提供一种方法来顺序访问聚合对象内的元素&#xff0c;而不暴露聚合对象…

LeetCode HOT 100 —— 448. 找到所有数组中消失的数字

题目 给你一个含 n 个整数的数组 nums &#xff0c;其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字&#xff0c;并以数组的形式返回结果。 思路 原地哈希&#xff08;简单模拟&#xff09;&#xff1a; 核心思路&#xff1a; 因为…

Python 和 PyQt5 实现打地鼠小游戏

Python 和 PyQt5 实现打地鼠小游戏 实现效果&#xff1a; 视频效果&#xff1a; https://live.csdn.net/v/264602https://live.csdn.net/v/264602 代码&#xff1a; import random import sysfrom PyQt5.QtCore import QBasicTimer, Qt, QTimer from PyQt5.QtGui import QCo…

CSS3【基础选择器、字体样式、文本样式、行高样式】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录基础选择器1. 标签选择器2. 类选择器3. id选择器字体和文本样式1.字体样式1.1 字体大小1.2 字体粗细1.3 字体样式&#xff08;是否倾斜&#xff09;1.4 常见字体系列…

JAVA中实现多线程-单例双重锁(DCL(Double Check Lock)双重锁检查)

一 .多线程 继承 Thread 类实现 Runnable 接口实现 Callable 接口线程池 重写run方法&#xff0c;创建对象&#xff0c;调用start()方法启动线程 1&#xff0c;新生状态 – 用new关键字建立一个线程后&#xff0c;该线程对象就处于新生状态。 – 处于新生状态的线程有自己的…

Netty前置知识

传统IO 这里以文件输入输出流&#xff1a;FileInputStream 、 FileOutputStream 来进行解释。由继承关系得知&#xff0c;这两个输入和输出类继承自 InputStream 和 OutputStream 这两个基础的输入、输出的抽象类&#xff0c;这时我们可以看到当我们需要读写文件的时候&#x…

leetcode--搜索

搜索1.深度优先搜索(DFS)&#xff08;1&#xff09;岛屿的最大面积(695)&#xff08;2&#xff09;省份数量&#xff08;3&#xff09;太平洋大西洋水流问题(417)2.回溯法&#xff08;1&#xff09;全排列(46)&#xff08;2&#xff09;组合(77)&#xff08;3&#xff09;单词搜…

C++ allocator设计内存管理器

文章目录allocator内存管理器基本属性类的设计关键功能的实现完整的内存管理器内存管理器的测试&#xff1a;设计自定义的String类。前情回顾&#xff1a; allocator内存管理类 allocator内存管理器 某些类需要在运行时分配可变大小的内存空间&#xff0c;一般来说我们使用容器…

从零搭建完整python自动化测试框架(UI自动化和接口自动化)

从零搭建完整python自动化测试框架&#xff08;UI自动化和接口自动化&#xff09; 文章目录 总体框架 PO模式、DDT数据驱动、关键字驱动 框架技术选择 框架运行结果 各用例对应的定义方式&#xff08;PO/DDT&#xff09; 测试执行结果 从零开始搭建项目 一、开发环境搭…

泪目,终于有P8大佬把困扰我多年的《计算机网络原理》全部讲明白了

前言 为什么网络协议这么重要呢&#xff1f;集群规模一大&#xff0c;我们首先想到的就是网络互通的问题&#xff1b;应用吞吐量压不上去&#xff0c;我们首先想到的也是网络互通的问题。所以&#xff0c;要成为技术牛人&#xff0c;搞定大系统&#xff0c;一定要过网络这一关&…

Mac怎么清理缓存?这两种方法都非常好用哦

与电脑系统或应用程序非常相似&#xff0c;您的Mac也有自己的系统缓存&#xff0c;它可以在后台临时存储数据&#xff0c;以加快软件安装速度并减少互联网数据使用量&#xff08;通过Apple&#xff09;。与电脑系统或应用程序类似&#xff0c;缓存数据可能会开始堆积——占用存…

unordered系列关联式容器以及哈希表原理实现

Ⅰ. unordered 系列关联式容器 在C98中&#xff0c;STL提供了底层为红黑树结构的一系列关联式容器&#xff0c;在查询时效率可达到 log2nlog_2 nlog2​n&#xff0c;即最差情况下需要比较红黑树的高度次&#xff0c;当树中的节点非常多时&#xff0c;查询效率也不理想。最好的…