4、动态代理的缓存机制

2023-11-20

1.背景

上一节大致介绍了Proxy动态代理的原理,从几个疑问上面分析,这一节介绍一下动态代理的缓存机制。网上的资源比较少。
可以怀着下面几个问题阅读源码

为什么要缓存
缓存的内容是什么
哪里调用的缓存
缓存的实现机制
缓存的过期机制

2.属性说明

Proxy涉及的缓存主要在java.lang.reflect.WeakCache这个类中
该类存在于jdk反射的包中
当生成代理对象的时候会用到

java.lang.reflect.Proxy#newProxyInstance
java.lang.reflect.Proxy#getProxyClass0
java.lang.reflect.WeakCache#get

看这个类的结构

//用了Reference记录引用队列,java gc时配合清除缓存用
private final ReferenceQueue<K> refQueue  = new ReferenceQueue<>();
//缓存的底层实现
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map = new ConcurrentHashMap<>();
//记录所有缓存中的CacheKey,配合缓存的过期机制
private final ConcurrentMap<Supplier<V>, Boolean> reverseMap = new ConcurrentHashMap<>();
//两个二元操作函数
private final BiFunction<K, P, ?> subKeyFactory;
private final BiFunction<K, P, V> valueFactory;

这里最重要的的是map,将key-value介绍如下

字段 意义 备注
key 通过classLoader来进行一级缓存 实际类型为CacheKey
value 二级缓存 后续会介绍

源码中把这个value的变量称为valuesMap,这里顺便借用,介绍这个“二级缓存”是干嘛的

字段 意义 备注
key 二级缓存的key,由classLoader和interfaces[]标识代理类 实际类型在java.lang.reflect.Proxy.KeyFactory#apply,为Key1或者Key2或者KeyX或者object,取决于代理类实现的接口数量
value 用于返回代理类(即Class<?>) 第一次存储实际类型为java.lang.reflect.WeakCache.Factory#Factory,之后取出时,都是java.lang.reflect.WeakCache.CacheValue

那么为什么要用缓存,缓存的内容是什么
缓存就是在动态代理生成代理类时,只用生成一次后面尽量直接复用

3.实现机制

java.lang.reflect.WeakCache#get

public V get(K key, P parameter) {
        Objects.requireNonNull(parameter);

        expungeStaleEntries();

        Object cacheKey = CacheKey.valueOf(key, refQueue);

        // lazily install the 2nd level valuesMap for the particular cacheKey
        ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
        if (valuesMap == null) {
            ConcurrentMap<Object, Supplier<V>> oldValuesMap
                = map.putIfAbsent(cacheKey,
                                  valuesMap = new ConcurrentHashMap<>());
            if (oldValuesMap != null) {
                valuesMap = oldValuesMap;
            }
        }

        // create subKey and retrieve the possible Supplier<V> stored by that
        // subKey from valuesMap
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        Supplier<V> supplier = valuesMap.get(subKey);
        Factory factory = null;

        while (true) {
            if (supplier != null) {
                // supplier might be a Factory or a CacheValue<V> instance
                V value = supplier.get();
                if (value != null) {
                    return value;
                }
            }
            // else no supplier in cache
            // or a supplier that returned null (could be a cleared CacheValue
            // or a Factory that wasn't successful in installing the CacheValue)

            // lazily construct a Factory
            if (factory == null) {
                factory = new Factory(key, parameter, subKey, valuesMap);
            }

            if (supplier == null) {
                supplier = valuesMap.putIfAbsent(subKey, factory);
                if (supplier == null) {
                    // successfully installed Factory
                    supplier = factory;
                }
                // else retry with winning supplier
            } else {
                if (valuesMap.replace(subKey, supplier, factory)) {
                    // successfully replaced
                    // cleared CacheEntry / unsuccessful Factory
                    // with our Factory
                    supplier = factory;
                } else {
                    // retry with current supplier
                    supplier = valuesMap.get(subKey);
                }
            }
        }
    }

注意下面几点

3.1 ConcurrentMap的get以及putIfAbsent的使用

可以看到有几处代码,形如

ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
        if (valuesMap == null) {
            ConcurrentMap<Object, Supplier<V>> oldValuesMap
                = map.putIfAbsent(cacheKey,
                                  valuesMap = new ConcurrentHashMap<>());
            if (oldValuesMap != null) {
                valuesMap = oldValuesMap;
            }
        }

这个是为了保证在多线程下,去获取同一个key,拿到的是同一个引用对象
可以理解像单例模式里面要判断两次,来保证不会多次生成对象

if(instance==null) {
  synchronize(XXX.class) {
    if(instance==null){
      instance = new XXX();
    }
  }
}

可以参考http://wxl24life.iteye.com/blog/1746794

3.2 为什么说第一次valuesMap里面value的实际类型和之后不一样

根据Supplier<V> supplier = valuesMap.get(subKey);中supplier值以及在while循环第几次,按照时间先后考虑

3.2.1 supplier==null,第一次while循环

这种出现在第一次请求中,二级缓存没有该key

逻辑为

supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
    // successfully installed Factory
    supplier = factory;
}

此时valuesMap里面的key是subKey
subKey是

Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));

根据java.lang.reflect.Proxy.KeyFactory#apply
subKey实际类型是Key1,Key2,Object或者KeyX,根据实现的接口数量决定

private static final class KeyFactory
        implements BiFunction<ClassLoader, Class<?>[], Object>
    {
        @Override
        public Object apply(ClassLoader classLoader, Class<?>[] interfaces) {
            switch (interfaces.length) {
                case 1: return new Key1(interfaces[0]); // the most frequent
                case 2: return new Key2(interfaces[0], interfaces[1]);
                case 0: return key0;
                default: return new KeyX(interfaces);
            }
        }
    }

3.2.2 supplier==null,第二次while循环(或者因其他原因,第n次while循环)

这种出现在第一次请求中,二级缓存没有该key,supplier被赋值之后
逻辑为

if (supplier != null) {
  // supplier might be a Factory or a CacheValue<V> instance
  V value = supplier.get();
  if (value != null) {
     return value;
  }
 }

supplier在第一次循环时,已经为factory了,这里调用get方法,就是进入
java.lang.reflect.WeakCache.Factory#get的逻辑

        @Override
        public synchronized V get() { // serialize access
            // re-check
            Supplier<V> supplier = valuesMap.get(subKey);
            if (supplier != this) {
                //supplier和当前supplier不等,验证不正确
                return null;
            }
            V value = null;
            try {
                //生成代理类对应的Class<?>信息
                value = Objects.requireNonNull(valueFactory.apply(key, parameter));
            } finally {
                if (value == null) { // remove us on failure
                    valuesMap.remove(subKey, this);
                }
            }
            assert value != null;
            CacheValue<V> cacheValue = new CacheValue<>(value);

            //把之前二级缓存的value替换为当前的CacheValue
            if (valuesMap.replace(subKey, this, cacheValue)) {
                // put also in reverseMap
                reverseMap.put(cacheValue, Boolean.TRUE);
            } else {
                throw new AssertionError("Should not reach here");
            }
            return value;
        }

其中, value = Objects.requireNonNull(valueFactory.apply(key, parameter));就是代理类

java.lang.reflect.Proxy.ProxyClassFactory#apply //返回Class<?>
//具体生成细节参考上一节

在生成value之后

//把当前的值即this(类型为Factory)替换为了cacheValue(类型CacheValue)
valuesMap.replace(subKey, this, cacheValue)

并且这一次get()最终返回的是value;也就是Class<?>

3.2.3 supplier!=null

出现在之后的某一次请求中,二级缓存已经有该key-value

if (supplier != null) {
  // supplier might be a Factory or a CacheValue<V> instance
  V value = supplier.get();
  if (value != null) {
     return value;
  }
 }

此时,supplier的类型为CacheValue
在Factory#get中生成时,就是
CacheValue<V> cacheValue = new CacheValue<>(value);
CacheValue#get方法,最终转入java.lang.ref.Reference#get,返回的就是这个value,即构造函数的参数value

3.3缓存的过期机制

这里用了java.lang.reflect.WeakCache.CacheKey是继承WeakReference的
即弱引用,在java gc中,满足适当的条件时,当一个referent是ROOT可达且为弱引用时,会将其放入对应的referenceQueue。
核心函数在

private void expungeStaleEntries() {
        CacheKey<K> cacheKey;
        while ((cacheKey = (CacheKey<K>)refQueue.poll()) != null) {
            cacheKey.expungeFrom(map, reverseMap);
        }
    }

可能会奇怪这里并没有refQueue的enqueue操作,这个操作是java gc的时候做的,这里不深究。
只要知道gc时,某些场景会进行refQueue的enqueue即可。
那么,之后再调用expungeStaleEntries()就能清除过期的缓存。

4. 思考

4.1 为什么要二级缓存

一级缓存用来区分classLoader,二级缓存用来区分实现的接口
生成类的缓存是按照ClassLoader来划分的,这是因为类的区分不仅根据类名还根据装载类的ClassLoader,也就是说同一个类被不同的ClassLoader加载,那么它们也是不同的.
参考
http://www.cnblogs.com/cruze/p/3843996.html
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.3

4.2 ConcurrentHashMap想要获取SingleValue的方法(严格来说是同一个引用)

如果putIfAbsent返回值不为null,那么返回该值,不要重新put
http://wxl24life.iteye.com/blog/1746794

4.3 二级缓存valueMaps的value的类型不一样

第一次是Factory,调用get()时,生成代理类信息之后,将CacheValue作为值替换了当前key对应的value
之后调用,都是CacheValue.get()返回java.lang.ref.Reference#get,最终就是CacheValue的构造函数参数,即代理类信息Class<?>

4.4 一级缓存的key-value与二级缓存的key-value

4.5 在'背景'中提出的问题,在上文中都有解释

4.6 缓存的过期机制

利用java.lang.reflect.WeakCache.CacheKey继承WeakReference,配合ReferenceQueue使用。
在java gc时某些条件下触发。

5.暂时不明白的问题

5.1 reverseMap的value有什么用?

感觉纯粹是利用ConcurrentHashMap的"Concurrent"的特性,value并没有什么用,代码里面并没有处理里面的value,都是Boolean.TRUE;

5.2 实现二级缓存的逻辑为什么要这么写,不能一步到位吗?

为什么先放入Factory,调用Factory.get时进行replace操作再放入CacheValue,不能一步到位写入CacheValue后面直接用吗

6. refer

concurrentHashMap 的putIfAbsent完成single value
http://wxl24life.iteye.com/blog/1746794

cglib的缓存说明
http://www.cnblogs.com/cruze/p/3843996.html
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.3

gc 与 reference
http://www.cnblogs.com/maxmys/p/3998842.html
http://hongjiang.info/java-referencequeue/
http://zhang-xzhi-xjtu.iteye.com/blog/413159


转摘于:https://www.jianshu.com/p/9f5566b5e7fb

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

4、动态代理的缓存机制 的相关文章

  • Go redis操作

    Go redis的操作 五种数据结构的基本操作汇总 一 String 操作 命令 说明 Set key value 给数据库中名称为 key 的 string 赋予值 valueget key 返回数据库中名称为 key 的 string
  • java并发编程笔记(四)--JMM内存模型

    1 计算机结构 输入设备 就是我们的鼠标 键盘 存储器 对应的就是我们的内存 缓存 运算器和控制器共同组成了cpu 而输出设备就比如显示屏 打印机 我们重点来聊一下缓存 2 缓存 其实 当我们说计算机运行效率低下 速度慢 往往不是cpu的锅
  • SpringBoot通过@Cacheable实现redis缓存机制

    SpringBoot通过注解实现redis缓存机制 Cacheable 查询 CachePut 修改 CacheEvict 删除 下面展示一些 内联代码片 Cacheable 查询 An highlighted block key 这边以d
  • RPC 开发系列一:RPC 基本介绍

    一 什么是 RPC RPC 的全称是 Remote Procedure Call 即远程过程调用 功能 屏蔽远程调用跟本地调用的区别 让我们感觉就是调用项目内的方法 隐藏底层网络通信的复杂性 让我们更专注于业务逻辑 二 RPC 通信流程 发
  • 老话新谈之缓存一致性

    前言 缓存一致性常见的更新策略也比较多 如先更新数据库再更新缓存 先删缓存再更新数据库等等 我在理解的时候有些混乱 所以这个文章提供了一些理解上的技巧去理解缓存一致性 为什么会有缓存一致性的问题 缓存与数据库是两套中间件 存在网络抖动之类的
  • Memcach基础使用

    memcache 基础课程 使用场景 memcache 服务器端的安装 推荐使用memcached memcached是memchache的升级版本 sudo su apt get install memcached usr bin mem
  • Redis事务

    7 Redis事务 7 1 背景 假如你给你朋友转账 此时你的账户会减少1bw 你朋友的账户会多1bw 此时如果你转账失败 但是你朋友的账户也多了1bw 此时这对于银行来说 这就是事故 说明你的程序存在很大漏洞 不能保证数据的原子性 此时就
  • windows下redis设置redis开机自启动方法(保姆级)

    1 找到Redis所在的目录 在文件路径框中输入cmd 2 进入到控制台下的Redis所在目录 输入下列命令 redis server service install redis windows service conf loglevel
  • java与redis连接过程中遇到问题

    java与redis连接过程中遇到问题 文章目录 java与redis连接过程中遇到问题 前言 一 redis是什么 特征 二 命令 1 redis通用命令 String类型常见命令 Hash常用命令 List常见命令 Set常见命令 三
  • Redis工具类(缓存操作,Object转换成JSON数据)

    依赖spring data redis 2 4 1 jar Component Data public class RedisUtils Autowired private RedisTemplate
  • Response响应方法详解

    Response 响应 服务器把请求的处理结果告知客户端 在B S架构中 响应就是把结果带回浏览器 响应对象 在项目中用于发送响应的对象 常用状态码 状态码 说明 200 执行成功 302 它和307一样 都是用于重定向的状态码 只是307
  • 5. 一线大厂高并发缓存架构实战与性能优化

    分布式缓存技术Redis 1 冷热数据分离 2 缓存设计 2 1 缓存击穿 失效 2 2 缓存穿透 2 3 缓存雪崩 3 大V直播带货导致线上商品系统崩溃原因分析 4 突发性热点缓存重建导致系统压力暴增问题 5 缓存数据库双写不一致问题 6
  • Redis数据类型

    文章目录 Redis介绍 RedisObject的结构 1 type 2 enconding 3 lru 4 refcount 5 prt Redis源码结构
  • Redis交互速度慢,频繁处理时经常报错 RedisSystemException: RedisException: Connection closed

    Redis交互速度很慢 达到几十到一百毫秒一次 且压力测试下经常报错 org springframework data redis RedisSystemException Redis exception nested exception
  • SpringBoot中项目启动及定时任务缓存数据库常用数据至内存变量并转换后高频调用

    场景 定时任务中需要获取数据库中数据进行数据转换成需要的格式并进行后续的业务处理 数据库中的数据更新频率不高 可将数据库中数据在项目启动后读取一遍数据 然后再通过定时任务定时查询数据库更新数据 实现数据库缓存的方式有多种 比如以下 Spri
  • 【Redis】Redis 红锁

    1 概述 上一篇文章 redis Redis 分布式锁 redis session Redlock 红锁 Zookeeper锁 本章节主要讲解redis中的红锁 假设我们有个客户端要获取锁 然后向master去获取锁 然后master会把锁
  • Redis生产环境最佳实践

    欢迎关注公众号 通过文章导读关注 11来了 及时收到 AI 前沿项目工具及新技术 的推送 发送 资料 可领取 深入理解 Redis 系列文章结合电商场景讲解 Redis 使用场景 中间件系列笔记 和 编程高频电子书 文章导读地址 点击查看文
  • Redis Geo:掌握地理空间数据的艺术

    欢迎来到我的博客 代码的世界里 每一行都是一个故事 Redis Geo 掌握地理空间数据的艺术 前言 Redis Geo基本概念 Geo模块的目的 工作原理 地理坐标系统
  • 浏览器缓存相关面试题一网打尽,理论结合实践,用代码学习缓存问题,建议关注+收藏,(含项目源代码)

    前言 浏览器缓存的问题是面试中关于浏览器知识的重要组成部分 也是性能优化题目的一部分 但是不要被吓到 我话放到这里 就那么点东西 我这一篇文章基本上就涵盖了所有相关的知识点 认真看一遍 所有的问题都是纸老虎 一 准备工作 1 1 拉取仓库
  • 软件测试/测试开发/全日制/测试管理丨Redis内存数据库

    Redis是一种开源 内存中的数据结构存储系统 它提供了高性能 灵活性和丰富的数据结构 以下是Redis内存数据库的基本介绍 键值存储 Redis基于键值对的存储模型 其中每个键都与一个特定的值相关联 这种简单的数据模型使其易于使用和理解

随机推荐

  • Scrum

    产品列表梳理会 Backlog Refinement Meeting 会议目的 Refinement 这个词是加工 提炼的意思 在scrum里 其实就是对下阶段的需求做一个讨论 澄清 细化的一个活动 希望通过这个活动 使得团队能对后续阶段的
  • 默认构造函数、拷贝构造函数、析构函数、赋值构造函数

    最近老是有人问我拷贝构造函数和赋值构造函数 说实话 我会用 但这个概念还真是搞不太清楚 真烦 概念问题少问我 学习笔记 1 析构函数 每个类只有一个析构函数 2 构造函数 每个类可以有多个构造函数 包括 默认构造函数 拷贝构造函数 赋值构造
  • Redis 7.0 核心技术、实战应用、面试题

    Redis 7 0 核心技术与实战应用 Redis 入门概述 01 Redis 是什么 Redis REmote Dictionary Server 远程字典服务器 官网介绍 https redis io docs about 官网定义 R
  • RabbitMQ消息丢失的场景,如何保证消息不丢失?(详细讲解,一文看懂)

    目录 一 RabbitMQ消息丢失的三种情况 二 RabbitMQ消息丢失解决方案 1 针对生产者 方案1 开启RabbitMQ事务 方案2 使用confirm机制 2 针对RabbitMQ 1 消息持久化 2 设置集群镜像模式 3 消息补
  • HDMI CEC协议

    1 前言 本文档仅作为本人记录使用 主要根据工作使用及 HDMI Specification 1 4a pdf 进行终结得出 若有不足会后续补充 2 CEC简介 CEC Consumer Electronics Control 是一套完整的
  • jenkins安装出现该实例似乎已离线等报错和如何卸载干净Jenkins的解决方案

    前段时间在准备使用Jenkins来实现Android自动化打包 但是在安装Jenkins的过程中出现了问题 在安装过程中出现 Jenkins实例似乎已离线 需要我配置代理 还有一个离线安装的文档 可是根据文档并没有明确说明怎么配置 然后我就
  • WPS Office 漏洞复现

    前言 此文章仅用于技术交流 严禁用于对外发起恶意攻击 一 产品简介 WPS Office是金山软件公司开发的 中国领先的办公软件套件 包含文字 表格和演示三个组件 支持创建 编辑各种文档 并具有强大的数据计算 统计和分析功能 其特点包括全面
  • Git提交代码步骤

    目录 1 Git提交代码步骤 1 1 第1步 同步远程仓库代码 git pull 1 2 第1步 查看当前状态 git status 1 3 第2步 提交代码到本地git缓存区 git add 1 4 第3步 推送代码到本地git库 git
  • 让csdn中的静态图动起(firemonkey)

    缘起 错过了登月50周年的日子 7 20 看到了如下卡通图童心大发 让它动起来 https blog csdn net csdnnews article details 96403350 代码下载 链接 https pan baidu co
  • CI/CD

    CICD 是 持续集成 Continuous Integration 持续交付和持续部署 Continuous Deployment 简称 指在开发过程中自动执行一系列从开发到部署的过程中 尽量减少人工的介入 CI CD AND CD CI
  • C++学习笔记12:输入输出流实例整理(文本文件读写,二进制文件读写,一组数据的文件读写,随机访问文件实例

    这也太难记了555老阔疼 文件读写示例 include
  • Kubernetes APIServer,Etcd,controller manager,scheduler 高可用原理

    高可用背后的原理 这两个月和博云合作的项目是要用于客户生产环境的 这个和我以前做的东西有很大的不同 所有基础架构必须给出高可用的解决方案 在这之前我只做过一些流量较小的用户产品或者一些原型项目 一开始基础架构都只给出了单节点的解决方案 结果
  • 微信小程序--给头像添加logo(生成海报同理)

    实现给图片添加logo或者生成海报 其原理是使用canvas 用canvas绘制出想要的图片进行保存 1 在wxml文件中添加canvas canvs层级太高 所以将它定位到屏幕外 不影响页面
  • 嵌入式资源网站

    原题地址 http blog csdn net ce123 article details 6724127 一 MailList 1 MailList大全 网址 http news gmane org 描述 可以查到绝大部分开源项目的Mai
  • LVGL学习(3):页面切换原理和页面管理实现

    在LVGL中 大多情况下是有多个页面的 一般来说页面的切换有两种情况 删除当前的页面 创建新页面加载 保留当前的页面 创建新页面加载 我们来分析一下这两种情况 比如页面1有一个列表框 有三个选项 每个选项对应进入一个页面 假设此时我们的焦点
  • VScode 怎么设置成中文

    1 打开 vscode 2 按快捷键 Ctrl Shift P 3 在 vscode 顶部会出现一个搜索框 4 在搜索栏输入 Configure Display Language 然后回车 5 vscode 里面就会打开一个语言配置文件 6
  • // SPDX-License-Identifier: MIT

    上述声明是指明你写的智能合约采用什么样的对外开放标准 该标准规定了别人是否拥有各种使用权利 其中 MIT 标准说明其他人可以随便用该源码 但出问题不负责
  • SQL各种日期转换代码大全

    select CONVERT varchar getdate 120 2004 09 12 11 06 08 select replace replace replace CONVERT varchar getdate 120 200409
  • html5 亲子模板,亲子阅读手抄报精美模板

    陪着孩子一起读 这点至关重要 不要丢下孩子一人读书 这样孩子很快就会失去读书的兴趣 今天学习啦小编给大家带来了亲子阅读手抄报精美模板 供大家阅读和参考 亲子阅读手抄报图片欣赏 亲子阅读手抄报图片1 亲子阅读手抄报图片2 亲子阅读手抄报图片3
  • 4、动态代理的缓存机制

    1 背景 上一节大致介绍了Proxy动态代理的原理 从几个疑问上面分析 这一节介绍一下动态代理的缓存机制 网上的资源比较少 可以怀着下面几个问题阅读源码 为什么要缓存 缓存的内容是什么 哪里调用的缓存 缓存的实现机制 缓存的过期机制 2 属