Java实战-基于JDK的LRU算法实现、优雅的实现代码耗时统计(Spring AOP、AutoCloseable方式)

发布时间 2023-05-31 15:40:30作者: 霸道流氓

场景

Java中基于JDK的LRU算法实现

LRU算法-缓存淘汰算法-Least recently used,最近最少使用算法

根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果有数据最近被访问过,那么将来被访问的几率也更高

在Java中可以利用LinkedHashMap容器简单实现LRU算法

LinkedHashMap底层就是用的HashMap加双链表实现的,而且本身已经实现了按照访问顺序的存储。

此外,LinkedHashMap中本身就实现了一个方法removeEldestEntry用于判断是否需要移除最不常读取的数,

方法默认直接返回false,不会移除元素,因此只需要重写这个方法,可以实现当缓存满之后,就移除不常用的数据。

新建LruCache类,使其继承LinkedHashMap并重写removeEldestEntry方法

import java.util.LinkedHashMap;
import java.util.Map;

public class LruCache<K,V> extends LinkedHashMap<K,V> {

    private int size;

    public LruCache(int size){
        //accessOrder:默认为false(即默认按照插入顺序迭代)为true时(按照访问顺序迭代,支持实现LRU算法时)
        //作为一般规则,默认负载因子(0.75)提供了一个很好的在时间和空间成本之间进行权衡。数值越高,空间开销,但增加查找成本.
        super(size,0.75f,true);
        this.size = size;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size()>size;
    }
}

调用示例

        LruCache<String,Integer> cache = new LruCache<>(4);
        for (int i = 0; i < 10; i++) {
            if(i == 5){
                cache.get("2");
            }
            cache.put(i+"",i);
            System.out.println(i+":"+cache);
            System.out.println("size:"+cache.size());
        }

注意上面的访问,当i == 5时,主动访问了一下2,主要是不希望淘汰掉它

运行结果

        //0:{0=0}
        //size:1
        //1:{0=0, 1=1}
        //size:2
        //2:{0=0, 1=1, 2=2}
        //size:3
        //3:{0=0, 1=1, 2=2, 3=3}
        //size:4
        //4:{1=1, 2=2, 3=3, 4=4}
        //size:4
        //5:{3=3, 4=4, 2=2, 5=5}
        //size:4
        //6:{4=4, 2=2, 5=5, 6=6}
        //size:4
        //7:{2=2, 5=5, 6=6, 7=7}
        //size:4
        //8:{5=5, 6=6, 7=7, 8=8}
        //size:4
        //9:{6=6, 7=7, 8=8, 9=9}
        //size:4

注:

博客:
https://blog.csdn.net/badao_liumang_qizhi

Java中优雅的实现代码耗时统计的方式

1、一般的写法

        long start = System.currentTimeMillis();
        try{
            costTimeHandler();
        } finally {
            System.out.println("cost:"+(System.currentTimeMillis() - start));
        }

优点是简单,适用范围广;缺点是侵入性强,大量的重复代码

2、使用Spring AOP实现代码耗时统计

使用Spring AOP,想要统计某个方法耗时,使用切面可以无侵入的实现。

首先定义自定义注解CountTime,声明周期是运行时,作用域是在方法上

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解,统计方法执行消耗时间
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CountTime {
}

然后定义切面类CountTimeAspect,用于处理请求时的内容。

定义一个切面,然后进行环绕通知,进行方法的统计时间,进行日志的输出打印。

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Slf4j
public class CountTimeAspect {

    //首先定义一个切点
    @org.aspectj.lang.annotation.Pointcut("@annotation(com.ruoyi.demo.actiondemo.CountTime)")
    public void countTime(){

    }

    @Around("countTime()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        Object obj = null;
        try{
            long beginTime = System.currentTimeMillis();
            obj = joinPoint.proceed();
            String methodName = joinPoint.getSignature().getName();
            String className = joinPoint.getSignature().getDeclaringTypeName();
            log.info("类:{},方法:{}耗时为:{}",className,methodName,System.currentTimeMillis()-beginTime);
        }catch (Throwable throwable){
            throwable.printStackTrace();
        }
        return obj;
    }
}

注意这里的包路径与上面的注解的路径一致

然后在需要统计的方法上添加注解CountTime,这里统计定时任务CountTimeTask中testCountTime执行的耗时

import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@EnableScheduling
public class CountTimeTask {

    @Scheduled(fixedRateString = "10000")
    @CountTime
    public void testCountTime(){
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试以上结果

 

3、java中使用实现AutoCloseable接口实现方法耗时统计

在JDK1.7引入了一个新的接口AutoCloseable,通常它的实现类配合try{}使用,

try(){}执行完毕之后,会调用方法AutoCloseable#close方法。

所以写一个Cost类实现AutoCloseable接口,创建时记录一个时间,

close方法中记录一个时间,并输出时间差值,将需要统计耗时的逻辑放在try(){}代码块中。

public class Cost implements AutoCloseable{

    private long start;

    public Cost(){
        this.start = System.currentTimeMillis();
    }
    @Override
    public void close() throws Exception {
        System.out.println("cost:"+(System.currentTimeMillis() - start));
    }
}

调用示例

        try(Cost cost = new Cost()){
            costTimeHandler();
        }

其中方法costTimeHandler()执行一个耗时操作

    public static void costTimeHandler(){
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

优点是简单,使用范围广泛,且适合统一管理;缺点是依然有代码侵入。

4、也可以使用Java Agent 探针(代理)的技术来实现

在JDK1.5时,引入了java.lang.Instrument包,该包提供了一些工具帮助开发人员在java程序运行时,

动态修改系统中的Class类型。其中,使用该软件包的一个关键组件为Java Agent,它的功能更像是Class类型的转换器,

可以在运行时接收程序外部请求,对Class类型进行修改。

实现方式自行学习。