# 十一、实战:实现高性能缓存

# 版本一:HashMap

/**
 * 第一代 Cache:HashMap
 *
 * @author Hedon Wang
 * @create 2021-03-26 10:47 AM
 */
public class Cache1G {

    private final HashMap<String, Integer> cache = new HashMap<>();

    /**
     * 计算
     *  1. 从缓存中拿
     *  2. 原始的计算,计算完放入缓存中
     */
    public Integer compute(String userId) throws InterruptedException{
        Integer result = cache.get(userId);
        //先检查 HashMap 里面有没有保存过之前的计算结果
        if(result == null){
            //如果缓存中找不到,那么需要现在计算一下结果,并且保存到 HashMap 中
            result = doCompute(userId);
            cache.put(userId,result);
        }
        return result;
    }

    /**
     * 进行计算
     */
    private Integer doCompute(String userId) throws InterruptedException{
        TimeUnit.SECONDS.sleep(5);
        return new Integer(userId);
    }


    /**
     * 测试
     */
    public static void main(String[] args) throws Exception{
        Cache1G cache1G = new Cache1G();
        System.out.println("开始计算了!!!");
        Integer compute1 = cache1G.compute("13");
        System.out.println("第一次计算结果:" + compute1);
        Integer compute2 = cache1G.compute("13");
        System.out.println("第二次计算结果:" + compute2);
    }

}

问题:

  1. 并发不安全
  2. 耦合度过高:计算和缓存是两个职责,应该拆分出来,计算只做计算的事,不关心缓存的情况。

# 版本二:装饰者模式降低耦合度,用 synchronized 解决线程安全问题

# 装饰者模式

参考:https://honeypps.com/design_pattern/decorator/

动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式想必生成子类更为灵活。

装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案

img

装饰模式中的角色有:

  1. 抽象构件角色(Component):给出一个抽象接口,以规范准备接受附加责任的对象。
  2. 具体构件角色(ConcreteComponent):定义一个将要接收附加责任的类。
  3. 装饰角色(Decorator):持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
  4. 具体装饰角色(ConcreteDecorator):负责给构件对象“贴上”附加的责任。

举个简单例子(类比《Head Fisrt》中的星巴克:吃杂粮煎饼,加个鸡蛋,加根火腿,加份辣条)

① 抽象构件角色

public abstract class Pancake{
  	//有名字
    protected String name;
    public String getName()
    {
        return this.name;
    }
  	//有价格
    public abstract BigDecimal getPrice();
}

② 具体构件角色

public class CoarsePancake extends Pancake{	
  	//名字叫杂粮煎饼
    public CoarsePancake(){
        this.name = "杂粮煎饼";
    }
		
  	//价格5块
    @Override
    public BigDecimal getPrice()
    {
        return new BigDecimal(5);
    }
}

③ 装饰角色

public abstract class Condiment extends Pancake{		
    public abstract String getName();

    public void sold()
    {
        System.out.println(getName()+":"+getPrice());
    }
}

④ 具体装饰角色

public class Egg extends Condiment{
  	
  	//包装 Pancake
    private Pancake pancake;
    public Egg(Pancake pancake)
    {
        this.pancake = pancake;
    }
		
  	//名字上多加一个 “鸡蛋”
    @Override
    public String getName()
    {
        return pancake.getName()+",加鸡蛋";
    }
		
  	//价格上多加1.5元
    @Override
    public BigDecimal getPrice()
    {
        return pancake.getPrice().add(new BigDecimal(1.5));
    }
}

public class Ham extends Condiment{
  
  	//包装 Pancake
    private Pancake pancake;
    public Ham(Pancake pancake)
    {
        this.pancake = pancake;
    }
  	
  	//名字上多家一个 “火腿”
    @Override
    public String getName()
    {
        return this.pancake.getName()+",加火腿";
    }
		
  	//价格上多加2块
    @Override
    public BigDecimal getPrice()
    {
        return pancake.getPrice().add(new BigDecimal(2));
    }
}


public class Lettuce extends Condiment{
  
  	//包装 Pancake
    private Pancake pancake;
    public Lettuce(Pancake pancake)
    {
        this.pancake = pancake;
    }
  
  	//名字上多加一个 “生菜”
    @Override
    public String getName()
    {
        return this.pancake.getName()+",加生菜";
    }
		
  	//价格上多加 1 块
    @Override
    public BigDecimal getPrice()
    {
        return pancake.getPrice().add(new BigDecimal(1));
    }
}

测试代码:

//杂粮煎饼
Pancake pancake = new CoarsePancake();
//鸡蛋包装杂粮煎饼
Condiment egg = new Egg(pancake);
//汉堡包包装鸡蛋
Condiment ham = new Ham(egg);
ham.sold();
//生菜包装汉堡包
Condiment lettuce = new Lettuce(ham);
lettuce.sold();

测试结果:

杂粮煎饼,加鸡蛋,加火腿:8.5
杂粮煎饼,加鸡蛋,加火腿,加生菜:9.5

适用场景

  1. 想透明并且动态地给对象增加新的职责的时候
  2. 给对象增加的职责,在未来存在增加或减少功能
  3. 用继承扩展功能不太现实的情况下,应该考虑用组合的方式
  4. 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者除掉一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了。
  5. 通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。

优点

  1. 通过组合而非继承的方式,实现了动态扩展对象的功能的能力。
  2. 有效避免了使用继承的方式扩展对象功能而带来的灵活性差,子类无限制扩张的问题。
  3. 充分利用了继承和组合的长处和短处,在灵活性和扩展性之间找到完美的平衡点。
  4. 装饰者和被装饰者之间虽然都是同一类型,但是它们彼此是完全独立并可以各自独立任意改变的。
  5. 遵守大部分 GRAP 原则和常用设计原则,高内聚、低偶合。

缺点

  1. 装饰链不能过长,否则会影响效率。
  2. 因为所有对象都是继承于 Component,所以如果 Component 内部结构发生改变,则不可避免地影响所有子类(装饰者和被装饰者),也就是说,通过继承建立的关系总是脆弱地,如果基类改变,势必影响对象的内部,而通过组合建立的关系只会影响被装饰对象的外部特征。
  3. 只在必要的时候使用装饰者模式,否则会提高程序的复杂性,增加系统维护难度。

JDK中的装饰模式

由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是Java I/O库的基本模式。

img

根据上图可以看出:

  • 抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型提供统一的接口。
  • 具体构件(ConcreteComponent)角色:由 ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream等类扮演。它们实现了抽象构件角色所规定的接口。
  • 抽象装饰(Decorator)角色:由 FilterInputStream 扮演。它实现了 InputStream 所规定的接口。
  • 具体装饰 (ConcreteDecorator) 角色:由几个类扮演,分别是 BufferedInputStream、DataInputStream 以及两个不常用到的类LineNumberInputStream、PushbackInputStream。
# 优化缓存

① 抽象构件角色

/**
 * ① 抽象构件角色
 *
 * 有一个计算函数 compute,用来代表耗时计算,
 * 每个计算器都要继承这个基类,
 * 这样就可以无侵入实现缓存功能
 */
public abstract class Computable<A,V> {
    public abstract V compute(A arg) throws Exception;
}

② 具体构件角色

/**
 * ② 具体的构件角色
 * 耗时计算的实现类,实现了  Computable 接口,
 * 但是本身不具备缓存能力,不需要考虑缓存的事情。
 */
public class ExpensiveFunction extends Computable<String, Integer> {

    /**
     * 计算过程
     */
    @Override
    public Integer compute(String arg) throws Exception {
        System.out.println("计算器正在计算 --------->>>>>>>");
        Thread.sleep(5000);
        return Integer.valueOf(arg);
    }
}

③ 装饰角色

/**
 * ③ 装饰角色
 */
public abstract  class Cache2GDecorator<A,V> extends Computable<A, V> {

    @Override
    public V compute(A arg) throws Exception {
        return compute(arg);
    }
}

④ 具体装饰角色

/**
 * ④ 具体装饰角色
 *
 *  给计算器自动添加缓存功能
 */
public class Cache2G<A,V> extends Cache2GDecorator<A, V> {

    /**
     *  包装计算器
     */
    private final Computable<A,V> computable;
    public Cache2G(Computable<A,V> computable){
        this.computable = computable;
    }

    /**
     * 缓存
     */
    private final Map<A,V> cache = new HashMap<>();


    /**
     * 给缓存添加计算功能,需要 synchronized 来保证线程安全
     */
    @Override
    public synchronized V compute(A arg) throws Exception {
        System.out.println("进入缓存机制...");
        V result = cache.get(arg);
        //1. 先判断缓存中有没有
        if (result == null){
            //2. 缓存中没有就自己计算
            result = computable.compute(arg);
            cache.put(arg,result);
        }
        return result;
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws Exception{
        Cache2G<String, Integer> cache2G = new Cache2G<>(new ExpensiveFunction());
        Integer res1 = cache2G.compute("666");
        System.out.println("第一次计算结果:" + res1);
        Integer res2 = cache2G.compute("666");
        System.out.println("第二次计算结果:" + res2);
    }
}

测试结果:

进入缓存机制...
计算器正在计算 --------->>>>>>>
第一次计算结果:666
进入缓存机制...
第二次计算结果:666

缺点:

  1. synchronized 性能差。
  2. 当多个线程同时想计算的时候,需要慢慢等待,严重时性能甚至比不用缓存更差。

# 版本三:ConcurrentHashMap 保证线程安全

/**
 * 第三代:使用 ConcurrentHashMap 保证线程安全
 */
public class Cache3G<A,V> extends Cache2GDecorator<A, V> {

    /**
     *  包装计算器
     */
    private final Computable<A,V> computable;
    public Cache3G(Computable<A,V> computable){
        this.computable = computable;
    }

    /**
     * 改用 ConcurrentHashMap 缓存
     */
    private final ConcurrentHashMap<A,V> cache = new ConcurrentHashMap<>();


    /**
     * 给缓存添加计算功能,无需 synchronized
     */
    @Override
    public V compute(A arg) throws Exception {
        System.out.println("进入缓存机制...");
        V result = cache.get(arg);
        //1. 先判断缓存中有没有
        if (result == null){
            //2. 缓存中没有就自己计算
            result = computable.compute(arg);
            cache.put(arg,result);
        }
        return result;
    }
}

缺点:

  1. 虽然 ConcurrentHashMap 解决了线程安全问题,但是在两个线程在同时判断 result == null 之后,还是都会去执行 result = computable.compute(arg),造成了重复计算,缓存的目的就是希望只计算一遍,下次直接查缓存,这就与我们的目的相悖了。

# 版本四:Future 解决重复计算

  1. 将 Map 中的 value 由 V 改成 Future<V>,这样放进缓存中的就是一个个的 Future 任务了,我们可以通过 future.get() 来获取计算的结果。
  2. put 的时候用 putIfAbsent() 这种原子操作,它只有在没有该 key 的情况下才去 put,不会造成覆盖。返回值是之前的元素,如果是 null 表示之前没有元素。
  3. 我们在 putIfAbsent 后判断之前是否有元素,如果没有元素,就调用 futureTask.run() 去执行 callable 中的 call() 方法进行计算,然后在方法 return 处用 future.get() 等待计算完成。
  4. 这样即实现了线程安全,也避免了重复计算。
/**
 * 第四代:使用 Future 解决重复计算问题
 */
public class Cache4G<A,V> extends Cache2GDecorator<A, V> {

    /**
     *  包装计算器
     */
    private final Computable<A,V> computable;
    public Cache4G(Computable<A,V> computable){
        this.computable = computable;
    }

    /**
     * 缓存,将 Map 中的 V 改成用 Future 包装 V 的 Future<V>
     */
    private final ConcurrentHashMap<A, Future<V>> cache = new ConcurrentHashMap<>();


    /**
     * 给缓存添加计算功能
     */
    @Override
    public V compute(A arg) throws Exception {
        System.out.println(Thread.currentThread().getName() + "进入缓存机制...");
        Future<V> future = cache.get(arg);
        //1. 先检查 Future 是不是 null,如果不是 null,说明前面已经有人在计算了,那就不计算了
        if (future == null){
            //2. 如果没人计算,那就再放入 Future 去计算
            Callable<V> callable = new Callable<V>() {
                @Override
                public V call() throws Exception {
                    return computable.compute(arg);
                }
            };
            //创建任务
            FutureTask<V> futureTask = new FutureTask<>(callable);
            //在计算之前将任务放入缓存当中,这样第二个线程要放进去的时候发现里面已经有了,就不会放进去了
            //putIfAbsent() 返回在放之前那个位置的数据,如果是 null,表示没有数据
            future = cache.putIfAbsent(arg,futureTask);
            if (future == null){
                //没有数据才进行计算
                future = futureTask;
                //执行计算
                System.out.println(Thread.currentThread().getName() + "从 FutureTask 调用了计算函数");
                futureTask.run();
            }
        }
        // future.get() 并不会立刻返回结果,而是阻塞到在任务执行完毕后才会返回结果
        return future.get();
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws Exception{
        Cache4G<String, Integer> cache4G = new Cache4G<>(new ExpensiveFunction());
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                    res = cache4G.compute("666");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                    res = cache4G.compute("666");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                    res = cache4G.compute("667");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
    }
}

测试结果:只计算了2次

Thread-0进入缓存机制...
Thread-1进入缓存机制...
Thread-0从 FutureTask 调用了计算函数
Thread-0计算器正在计算 --------->>>>>>>
Thread-2进入缓存机制...
Thread-2从 FutureTask 调用了计算函数
Thread-2计算器正在计算 --------->>>>>>>
Thread-2计算结果:667
Thread-0计算结果:666
Thread-1计算结果:666

# 版本五:解决计算中抛出的异常 ExecutionException

添加一个可能抛出异常的计算器:

/**
 *  ② 具体的构件角色
 *  耗时计算的实现类,实现了  Computable 接口,
 *  但是本身不具备缓存能力,不需要考虑缓存的事情。
 *
 *  跟之前的计算器相比:!!!!它有一定的几率计算失败!!!!
 */
public class ComputeFail extends Computable<String,Integer>{
    @Override
    public Integer compute(String arg) throws Exception {
        double random = Math.random();
        if (random > 0.5){
            throw  new IOException("读取文件出错 T_T");
        }
        Thread.sleep(3000);
        return Integer.valueOf(arg);
    }
}

不同的异常处理逻辑是不一样的:

  • CancellationException 取消异常和 InterruptedException 中断异常是人为的,那么我们应该立即终止任务。
  • ExecutionException 计算异常是计算错误,不是人为的,而且如果我们明确知道多试几次就可以得到答案,那么我们的逻辑应该是重试,尝试多次直到正确结果出现。
  • 在这里我们家上 while(true) 来保证计算出错的话就进入下一个循环,直到计算成功,如果不是计算错误的话,那么就捕获异常,输出日志,然后再 throw e 把异常抛给调用者。

但是这就会造成一个问题了:==缓存污染==,重试的过程中我们会一直往 Map 中放入 Future,但是之前的 Future 还一直在,它的污染还一直在。我们应该先清除它 ——> 一旦发生异常了就应该移除掉该 Future。

/**
 * 第五代:解决计算过程中的异常
 */
public class Cache5G<A,V> extends Cache2GDecorator<A, V> {

    /**
     *  包装计算器
     */
    private final Computable<A,V> computable;
    public Cache5G(Computable<A,V> computable){
        this.computable = computable;
    }

    /**
     * 缓存,将 Map 中的 V 改成用 Future 包装 V 的 Future<V>
     */
    private final ConcurrentHashMap<A, Future<V>> cache = new ConcurrentHashMap<>();


    /**
     * 给缓存添加计算功能
     */
    @Override
    public V compute(A arg) throws Exception {
        /**
         * 5G:计算出错会一直重试
         *
         * 如果是 CancellationException 或者 InterruptedException 那么会 throw 给调用方,退出循环
         * 如果是 ExecutionException 那么就重试
         */
        while (true){
            System.out.println(Thread.currentThread().getName() + "进入缓存机制...");
            Future<V> future = cache.get(arg);
            //1. 先检查 Future 是不是 null,如果不是 null,说明前面已经有人在计算了,那就不计算了
            if (future == null){
                //2. 如果没人计算,那就再放入 Future 去计算
                Callable<V> callable = new Callable<V>() {
                    @Override
                    public V call() throws Exception {
                        return computable.compute(arg);
                    }
                };
                //创建任务
                FutureTask<V> futureTask = new FutureTask<>(callable);
                //在计算之前将任务放入缓存当中,这样第二个线程要放进去的时候发现里面已经有了,就不会放进去了
                //putIfAbsent() 返回在放之前那个位置的数据,如果是 null,表示没有数据
                future = cache.putIfAbsent(arg,futureTask);
                if (future == null){
                    //没有数据才进行计算
                    future = futureTask;
                    //执行计算
                    System.out.println(Thread.currentThread().getName() + "从 FutureTask 调用了计算函数");
                    futureTask.run();
                }
            }
            // future.get() 并不会立刻返回结果,而是阻塞到在任务执行完毕后才会返回结果
            try {
                return future.get();
            }catch (CancellationException e){
                System.out.println("计算被取消了...");
                //5G: 清除污染
                cache.remove(arg);
                throw e;
            }catch(InterruptedException e){
                System.out.println("计算被中断了...");
                //5G: 清除污染
                cache.remove(arg);
                throw e;
            }catch (ExecutionException e){
                System.out.println("计算错误,重试!!!!!");
                //5G: 清除污染
                cache.remove(arg);
            }
        }
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws Exception{
        //使用可能抛异常的计算器
        Cache5G<String, Integer> cache5G = new Cache5G<>(new ComputeFail());
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                    res = cache5G.compute("666");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                    res = cache5G.compute("666");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                    res = cache5G.compute("667");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
    }
}

测试结果:

Thread-0进入缓存机制...
Thread-1进入缓存机制...
Thread-2进入缓存机制...
Thread-2从 FutureTask 调用了计算函数
Thread-1从 FutureTask 调用了计算函数
Thread-2计算器正在计算 --------->>>>>>>
Thread-1计算器正在计算 --------->>>>>>>
计算错误,重试!!!!!
计算错误,重试!!!!!
Thread-1进入缓存机制...
Thread-0进入缓存机制...
Thread-1从 FutureTask 调用了计算函数
Thread-1计算器正在计算 --------->>>>>>>
Thread-2计算结果:667
Thread-1计算结果:666
Thread-0计算结果:666

# 版本六:引入缓存过期功能

  1. 因为计算结果可能会发生改变,我们需要为每个结果指定过期时间,并定期扫描过期的元素,及时更新数据,即使计算结果没有改变也可以减少内存压力。
  2. 可以利用定时线程池 ScheduledExecutorService 来实现这个功能。
  3. 如果每个数据的过期时间都一样的话,很容易造成集体过期的情况,所以我们样例中随机生成过期时间。
/**
 * 第六代:出于安全性考虑,缓存需要设置有效期
 * 
 * @author Hedon Wang
 * @create 2021-03-26 3:34 PM
 */
public class Cache6G<A,V> extends Cache2GDecorator<A, V> {

    /**
     *  包装计算器
     */
    private final Computable<A,V> computable;
    public Cache6G(Computable<A,V> computable){
        this.computable = computable;
    }

    /**
     * 缓存,将 Map 中的 V 改成用 Future 包装 V 的 Future<V>
     */
    private final ConcurrentHashMap<A, Future<V>> cache = new ConcurrentHashMap<>();


    /**
     * 给缓存添加计算功能
     */
    @Override
    public V compute(A arg) throws Exception {
        /**
         * 5G:计算出错会一直重试
         *
         * 如果是 CancellationException 或者 InterruptedException 那么会 throw 给调用方,退出循环
         * 如果是 ExecutionException 那么就重试
         */
        while (true){
            System.out.println(Thread.currentThread().getName() + "进入缓存机制...");
            Future<V> future = cache.get(arg);
            //1. 先检查 Future 是不是 null,如果不是 null,说明前面已经有人在计算了,那就不计算了
            if (future == null){
                //2. 如果没人计算,那就再放入 Future 去计算
                Callable<V> callable = new Callable<V>() {
                    @Override
                    public V call() throws Exception {
                        return computable.compute(arg);
                    }
                };
                //创建任务
                FutureTask<V> futureTask = new FutureTask<>(callable);
                //在计算之前将任务放入缓存当中,这样第二个线程要放进去的时候发现里面已经有了,就不会放进去了
                //putIfAbsent() 返回在放之前那个位置的数据,如果是 null,表示没有数据
                future = cache.putIfAbsent(arg,futureTask);
                if (future == null){
                    //没有数据才进行计算
                    future = futureTask;
                    //执行计算
                    System.out.println(Thread.currentThread().getName() + "从 FutureTask 调用了计算函数");
                    futureTask.run();
                }
            }
            // future.get() 并不会立刻返回结果,而是阻塞到在任务执行完毕后才会返回结果
            try {
                return future.get();
            }catch (CancellationException e){
                System.out.println(Thread.currentThread().getName() + "计算被取消了...");
                //清除污染
                cache.remove(arg);
                throw e;
            }catch(InterruptedException e){
                System.out.println(Thread.currentThread().getName() + "计算被中断了...");
                //清除污染
                cache.remove(arg);
                throw e;
            }catch (ExecutionException e){
                System.out.println(Thread.currentThread().getName() + "计算错误,重试!!!!!");
                //清除污染
                cache.remove(arg);
            }
        }
    }

    /**
     * 6G:引入缓存过期功能
     */
    public final static ScheduledExecutorService service = Executors.newScheduledThreadPool(5);

    /**
     * 6G:具备清除过期缓存的功能
     * @param expire  过期时间
     */
    public V compute(A arg, long expire) throws Exception{
        if (expire > 0){
            service.schedule(new Runnable() {
                @Override
                public void run() {
                   expire(arg);
                }
            },expire,TimeUnit.MILLISECONDS);
        }
        return compute(arg);
    }

    /**
     * 6G:过期清除方法
     * @param arg
     */
    private synchronized void expire(A arg){
        Future<V> future = cache.get(arg);
        if (future != null){
            if (!future.isDone()){
                System.out.println(future+"任务已过期,不需要再计算了");
                future.cancel(true);
            }
            cache.remove(arg);
            System.out.println(future + "过期时间到,缓存被清除");
        }
    }

    /**
     * 6G:可以随机设置缓存有效期的计算方法
     */
    public V computeRandomExpire(A arg) throws Exception {
        long randExpire = (long)Math.random() * 10000;
        return compute(arg,randExpire);
    }


    /**
     * 测试
     */
    public static void main(String[] args) throws Exception{
        //使用可能抛异常的计算器
        Cache6G<String, Integer> cache6G = new Cache6G<>(new ComputeFail());
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                  	//缓存有效期5s
                    res = cache6G.compute("666",5000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
      	//休眠10s,让第一个缓存过期
        Thread.sleep(10000);
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                  	//缓存有效期无限
                    res = cache6G.compute("666");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Integer res = null;
                try {
                    res = cache6G.compute("667");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +"计算结果:" + res);
            }
        }).start();
    }
}

测试结果:

Thread-0进入缓存机制...
Thread-0从 FutureTask 调用了计算函数
Thread-0计算器正在计算 --------->>>>>>>
Thread-0计算结果:666
java.util.concurrent.FutureTask@65b21710过期时间到,缓存被清除
Thread-1进入缓存机制...
Thread-1从 FutureTask 调用了计算函数
Thread-1计算器正在计算 --------->>>>>>>
Thread-2进入缓存机制...
Thread-2从 FutureTask 调用了计算函数
Thread-2计算器正在计算 --------->>>>>>>
Thread-1计算结果:666
Thread-2计算结果:667
上次更新: 9/17/2021, 12:28:06 PM