编写一个用于缓存的Map进一步认识软引用和引用队列


前言

其实本来只是觉得springboot自带的缓存api不太好用,想自己也写一个基于注解的缓存库来优化一下自己其他代码。在之前的代码中,我都会使用hashmap来做一些本地缓存和redis做一些公用缓存,虽然代码简单,但是也出现了挺多类似的if代码,这对于有严重代码洁癖的我是无法忍受的,终于抽出了一片的时间来实现这个计划,然而,在这个看似简单的计划中,我越想越多,越陷越深,竟然是缓存,他的作用是加速,而且不是必须的,如果一直存在的话可能会出现一系列问题,我们应该和redis那样根据一些规则清理一下这些缓存,在java中最简单的方法大家都会想到使用弱引用和软引用了,在犹豫了一分钟后,我在软引用和弱引用中选择了软引用,可能有些读者会对这些方面的知识有点忘记了,我下面简单概括一下

强引用:就是我们正常情况下的引用,虚拟机宁愿自己崩掉都不会考虑清理被有强引用指向的对                   象,所以我们写代码时候不会遇到莫名其妙对象没了的情况

软引用:如果一个对象只被弱引用指向,那么当虚拟机内存不够的时候就会回收这个对象,所以                  他也算比较安全的,虚拟机内存不满就没事,

弱引用:虚拟机进行垃圾回收的时候就会吧他给清理,不管虚拟机内存有没有满。

虚引用:有和没有都一样,唯一的作用就是配合引用队列(可能有些人对引用队列不是很清楚,                   通过这个文章可能会有进一步的了解)


文章说明

本文章用的我个人比较喜欢的一种风格,叙述型代码,和讲故事一样,并不会直接复制粘贴全部代码,有需求的可以和我说下,我们从map入手,制作一个当虚拟机内存满的时候,所有value会被清掉的map,这种map很适合做缓存,想象一下,如果我们不用redis等缓存数据库,而是自己写一个map来缓存服务器的token,不用每次都花大量cpu资源解析token,加速客户端响应,随着token越来越多,很多过期的token的缓存我们也很难去清理他,久而久之,虚拟机就内存溢出了,其实写一个这种map最好的方法是完全重写map,然后在hashmap上复制一些结构和算法实现相关的代码出来(自己写的就更好了),但是为了让文章阅读简单一点,我写了一个softMap类,聚合了一个hashmap,通过代理这个hashmap来实现这个功能,听起来不错,但是有个弊端,就是map的entry,hashmap这东西设计出来就不想让别人继承,我重写的entrySet方法深入输出的也符合预期,但是其实违法了类重写的规规范,有些人也应该能看得出来,但这不是重点,

public class SoftHashMap<K, V> implements Map<K, V> {


    private final ReferenceQueue<V> queue;

    private HashMap<K, DataNode> content;

    
   .....
}

这里的DataNode可以理解为对原本的V的一个封装,以及后面的queue会在后面做出详细使用。

给自己打一段广告

本人大四刚刚毕业,是21届的一个热血青年,对于java有很大的热爱和野心,刚刚从原本要转正的实习公司辞职,原因是因为我偏向于在北上深发展,也想拥有大点的平台,目前还在寻找工作,只想做java,最近去面试了平安寿险的java开发,本来以为稳过的面试竟然挂了,自信心受挫,我面试大部分的问题我都能回答得比较清楚,但是确实缺乏一些大型项目的经验, 现在很多公司都停止校招了, 社招相对于有开发经验得人也没有什么优势(但是我可以肯定我对于计算机技术的热爱肯定远超大部分人,有时候我感觉已经算是痴迷的境界了),希望大佬们可以的话可以提供我一点机会。

一、先理解SoftReference

       竟然要用到软引用,我们的核心当然就是SoftReference了,他是目前jdk用来实现软引用的主要方式,对应的还要wakeReference等等,其实本质都差不多,但是,对他的理解非常重要,很多人对各种引用的理解都卡在了Reference对象上。

       首先,最重要的一点,他虽然叫软引用类,但是他是个普通的类,没什么很特殊的地方,并不是说Object o = new SoftReference(xx); o就是一个软引用,这是错误的,o仍然是一个强引用,这个SoftReference对象不自己动手死也不回给清理的,

      他的特点是。这个SoftReference里面会聚合了一个对象,可以通过get方法获取到他,但是聚合这个对象的引用是软引用,也就是说,如果虚拟机内存满了,full gc清理后, 这个SoftReference对象还在,但是他包装的对象,就消失了,变成了null,你的get方法就再也找不到他了(这里排除被包装的对象被其他地方引用的情况)。如果还是听不太明白,我可以举个例子,假设有一个map的key是String对象,值是SoftReference对象(SoftReference的泛型代表了他包装的对象的类型)我们执行

Map<String, SoftReference<String>> map = new HashMap<>()
map.put("a", new SoftReference("v"));

(注意,实际上应该是用new String("v") 为了增加读者体验这里用"v"来代替,不再内容之内,先不解释为什么)

通常我们使用map.get("a") 可以得到对应的SoftReference对象,然后调用该对象的get()就能得到字符串"v"

在一次full gc之后, 我们用map.get("a")仍然得到这个SoftReference对象,但是调用他的get方法却得到了null, 因为map的value是强引用,SoftReference作为一个普通的对象并不能改变这一个事实,但是他改变不了世界,但是确可以改变他自己!我用代码说明

//一次full gc过后
SoftRenerce s = map.get("a");   //仍然是原来的对象没有一丝丝改变
s.get()                         //null

没错,那个"v"String对象就这样去世了。

     这时候你可能会提出疑问,虽然垃圾清理后Reference包装的对象会消失,但是Reference还在啊,他仍然了占着空间,浪费内存,有何意义,你提出这个疑问的话就说明前面的你都理解了,所以reference经常会和引用队列ReferenceQueue一起使用,来实现他存在的意义, 如果没有下面要说的引用队列,reference这东西真没多大价值

二、ReferenceQueue

首先他没说明大不了的,他本质上就是个队列,不过有个坑,他里面装的并不是他的泛型所指的对象,而是Reference对象,比如ReferenceQueue里面并不是String,而是Reference,  我们并不用特意用它去装Reference对象,JDK为我们提供了自动化使用方式,我们在创建Reference对象时,只要在构造函数传入我们的ReferenceQueue,比如

ReferenceQueue q = new ReferenceQueue<>()
SoftReference s = new SoftReference("v", q)
 

这样当s包装的值被虚拟机的gc清理后,s就会自动加入q队列里面去(这里我大胆猜测他是使用的的观察者模型实现的),但是queue加入的是s本身,人死不能复生,你就别指望s.get()会回复成原来的"v",他还是null,队列里存储的是值被清理后的reference,并不可能让里面的值复活,也就是说,我们通过搜索这个队列的时候,发现s在里面,说明s包装的值以及被清理了,就算你调用s.get()方法得到的也是null,我们得到的消息只有s里面的值没了,其他的消息,包括之值,我们一无所知。读完了上面, 你应该了解了queue的机制,但是你仍然不知道怎么使用他,下面,就来说怎么利用他。

1 .使用ReferenceQueue

1 .1  增强Reference, 设计DataNode

所以,ReferenceQueue的设计是提供了一种最小消息的模式,你只知道queue里面的reference的值已经被清理了,其他的一无所知,但是,却不妨碍我们增强他,他没有提供的消息,我们自己来提供,我们第一步就是增强jdk给我们提供的Reference,增强最经常用的应该就是继承和聚合了,很多大佬都推荐聚合而反对继承,我也也深深收到他们影响,但是我的身体很诚实,为了节约篇幅....

我们先定义一个DataNode内部类,他继承了Reference

private class DataNode extends SoftReference<V>{
    private K k;

    public DataNode(K k, V v){
        super(v, queue);
        this.k = k;
    }
}

queue就是我们文中最上面的ReferenceQueue了,也就是说,DataNode本身就是一个Reference

而且他还增加了一个k的值,记录map上的key的值(你可以先猜猜他的作用),他关联了前面的那个queue,也就是说,他包含的v一旦被销毁,他就会进入那个queue队列中,我再强调一下,是这个空心的reference,而不是他包装的v进去,他的v已经彻彻底底走了。

1 .2  代理内聚的的hashMap

核心内容如下

private final ReferenceQueue<V> queue;

    /*
     *实际上的map对象,我们对他进行代理,使得更加易用
     */
    private HashMap<K, DataNode> content;

    public SoftHashMap() {
        queue = new ReferenceQueue<>();
        content = new HashMap<>();
    }


    public V get(Object key) {

        DataNode dataNode = content.get(key);
        return dataNode.get();
    }

    @Override
    public V put(K key, V value) {
        V oldValue = get(key);
        DataNode dataNode = new DataNode(key, value);
        content.put(key, dataNode);
        return oldValue;
    }

这实际上是一个我简化后的伪代码,这个虽然能运行,但是有问题,但是大家看得懂大概意思就够了,get方法吧content的dataNode开封了,put也吧数据封装成dataNode了并且存入了content上了,

1 .3  使用DataNode达到自动清除无用key的效果

我们可以注意到,我们的DataNode是包含了这个值的key消息的,DataNode的值被清理后,他就会进入queue,他包装的v由于是softReference的软引用,已经没了,但是他的k(保存了key消息)却是强引用,我们仍然能在queue中拿出k的值,然后调用content.remove(k),让这个DataNode(也就是reference)断掉和map的引用关系,而且将他从queue弹出,从此就再也没人能访问他了,也就是说这个reference对象失去了所有引用了,下次gc就会被回收,我们调用map.get(k)的时候也再也拿不到这个reference了。这是一种自杀的方式,断掉和增加有关的引用,

我们编写一个clear的方法

private void clear() {
        /*
         * poll()和remove()都将移除并且返回,
         * 但是在poll()在队列为空时返回null,
         * 而remove()会抛出NoSuchElementException异常。
         */
    while(true){
        DataNode dataNode = (DataNode)queue.poll();
        if(dataNode != null){
            content.remove(dataNode.k);
        }else{
            break;
        }
    }

}

然后,我们在这个softMap的所有方法调用前面都加入clear()操作,就能保证每次都会吧无用的key清掉不浪费内存了(这并不会造成太多性能问题, 因为大部分都只是解除引用的操作,而且queue没有值的时候也就判断了一波)

好了,这个SoftMap的核心和思路大部分都这样实现了,其实最主要的还是学习reference的概念和解决问题的思路,需要具体代码的可以私聊我,我已经把他发布到github上了


总结

希望这篇文章对你们有所帮助,也希望能够多认识一些编码爱好者,一起学习共同交流,也希望大佬们大佬们能注意到我的求职消息,给予一些机会

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值