利用DelayQueue实现一个可定时的缓存

2021-08-05

利用DelayQueue实现一个可定时的缓存,使用单例模式,以便调用的时候都是同一个对象,定时器用DelayQueue来实现,还可以支持多线程情况下的调用

DelayQueue简介

DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。注意:不能将null元素放置到这种队列中。
因为DelayQueue是基于PriorityQueue实现的,PriorityQueue底层是一个堆,可以按时间排序,所以等待队列本身只需要维护根节点的一个定时器就可以了,而且插入和删除都是时间复杂度都是logn,资源消耗很少,作为一个缓存的定时装置是非常适合的
使用 DelayQueue 需要传入一个Delayed对象,要实现两个方法
1 getDelay(TimeUnit unit) 表示当前对象关联的延时,在本例中表示生存时间
2 compareTo(Delayed o) 堆在进行删除和插入时需要进行对比,所以要传入一个比较器

实例展示

public class ExpiryMap {

    private static final Logger log = LoggerFactory.getLogger(ExpiryMap.class);

    private ExpiryMap() {
    }
    private static volatile ExpiryMap expiryMap;
    /**
     * 键值对集合
     */
    private final static Map<String, Object> map = new HashMap<>();
    /**
     * 使用阻塞队列中的等待队列,因为DelayQueue是基于PriorityQueue实现的,而PriorityQueue底层是一个最小堆,可以按过期时间排序,
     * 所以等待队列本身只需要维护根节点的一个定时器就可以了,而且插入和删除都是时间复杂度都是logn,资源消耗很少
     */
    private final static DelayQueue<DelayData<String>> delay = new DelayQueue<>();

    //使用单例模式,加上双重验证,可适用于多线程高并发情况
    public static  ExpiryMap getInstance() {
        if (expiryMap == null) {
            synchronized (ExpiryMap.class) {
                if (expiryMap == null) {
                    expiryMap = new ExpiryMap();
                    //生成一个线程扫描等待队列的值
                    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
                    scheduledExecutorService.execute(()->{
                        while (true) {
                            try {
                                //此方法是阻塞的,没有到过期时间就阻塞在这里,直到取到数据
                                DelayData<String> take = delay.take();
                                map.remove(take.data);
                            } catch (InterruptedException e) {
                                log.error("ExpiryMap线程被打断");
                            }
                        }
                    });
                }
            }
        }
        return expiryMap;
    }

    /**
     * 添加缓存
     *
     * @param key    键
     * @param data   值
     * @param expire 过期时间,单位:毫秒, 0表示无限长
     */
    public void put(String key, Object data, long expire) {
        map.put(key,data);
        //当等于0时,就不把过期时间放进队列里了,值在代码运行期间会一直存在
        if (expire != 0) {
            delay.offer(new DelayData<>(key, System.currentTimeMillis() + expire));
        }
    }

    /**
     * 读取缓存
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return map.get(key);
    }

    /**
     * 清除缓存
     *
     * @param key 键
     * @return 值
     */
    public Object remove(String key) {
        return map.remove(key);
    }

    /**
     * 查询当前缓存的键值对数量
     * @return 数量
     */
    public int size() {
        return map.size();
    }
    //试下Delayed接口
    static class DelayData<T> implements Delayed{
        private T data;
        //到期时间
        private long expire;

        public DelayData(T data, long expire) {
            this.data = data;
            this.expire = expire;
        }

        //如果返回小于0就代表过期了
        @Override
        public long getDelay(TimeUnit unit) {
            //expire是过期时的时间
            long diffTime= expire- System.currentTimeMillis();
            return unit.convert(diffTime,TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            return (int)(this.expire - ((DelayData<T>) o).getExpire());
        }

        public T getData() {
            return data;
        }

        public void setData(T data) {
            this.data = data;
        }

        public long getExpire() {
            return expire;
        }

        public void setExpire(long expire) {
            this.expire = expire;
        }
    }

写一个测试方法

分别像缓存中放入3个值,时间分别是1s,2s,3s,每过1s,打印下结果

    public static void main(String[] args) throws InterruptedException {
        ExpiryMap instance = getInstance();
        instance.put("key1","key1",1000);
        instance.put("key2","key2",2000);
        instance.put("key3","key3",3000);
        System.out.println(instance.get("key1"));
        System.out.println(instance.get("key2"));
        System.out.println(instance.get("key3"));
        Thread.sleep(1000);
        System.out.println("-----------------");
        System.out.println(instance.get("key1"));
        System.out.println(instance.get("key2"));
        System.out.println(instance.get("key3"));
        Thread.sleep(1000);
        System.out.println("-----------------");
        System.out.println(instance.get("key1"));
        System.out.println(instance.get("key2"));
        System.out.println(instance.get("key3"));
        Thread.sleep(1000);
        System.out.println("-----------------");
        System.out.println(instance.get("key1"));
        System.out.println(instance.get("key2"));
        System.out.println(instance.get("key3"));
    }

输出结果

可以看出,缓存每过一秒就会被删除一个元素,删除时间是根据传入的过期时间清除的

key1
key2
key3
-----------------
null
key2
key3
-----------------
null
null
key3
-----------------
null
null
null