# 十一、实战:实现高性能缓存
# 版本一: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);
}
}
问题:
- 并发不安全
- 耦合度过高:计算和缓存是两个职责,应该拆分出来,计算只做计算的事,不关心缓存的情况。
# 版本二:装饰者模式降低耦合度,用 synchronized 解决线程安全问题
# 装饰者模式
参考:https://honeypps.com/design_pattern/decorator/
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式想必生成子类更为灵活。
装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。
装饰模式中的角色有:
- 抽象构件角色(Component):给出一个抽象接口,以规范准备接受附加责任的对象。
- 具体构件角色(ConcreteComponent):定义一个将要接收附加责任的类。
- 装饰角色(Decorator):持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
- 具体装饰角色(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
适用场景
- 想透明并且动态地给对象增加新的职责的时候
- 给对象增加的职责,在未来存在增加或减少功能
- 用继承扩展功能不太现实的情况下,应该考虑用组合的方式
- 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者除掉一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了。
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
优点
- 通过组合而非继承的方式,实现了动态扩展对象的功能的能力。
- 有效避免了使用继承的方式扩展对象功能而带来的灵活性差,子类无限制扩张的问题。
- 充分利用了继承和组合的长处和短处,在灵活性和扩展性之间找到完美的平衡点。
- 装饰者和被装饰者之间虽然都是同一类型,但是它们彼此是完全独立并可以各自独立任意改变的。
- 遵守大部分 GRAP 原则和常用设计原则,高内聚、低偶合。
缺点
- 装饰链不能过长,否则会影响效率。
- 因为所有对象都是继承于 Component,所以如果 Component 内部结构发生改变,则不可避免地影响所有子类(装饰者和被装饰者),也就是说,通过继承建立的关系总是脆弱地,如果基类改变,势必影响对象的内部,而通过组合建立的关系只会影响被装饰对象的外部特征。
- 只在必要的时候使用装饰者模式,否则会提高程序的复杂性,增加系统维护难度。
JDK中的装饰模式
由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是Java I/O库的基本模式。
根据上图可以看出:
- 抽象构件(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
缺点:
- synchronized 性能差。
- 当多个线程同时想计算的时候,需要慢慢等待,严重时性能甚至比不用缓存更差。
# 版本三: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;
}
}
缺点:
- 虽然 ConcurrentHashMap 解决了线程安全问题,但是在两个线程在同时判断 result == null 之后,还是都会去执行 result = computable.compute(arg),造成了重复计算,缓存的目的就是希望只计算一遍,下次直接查缓存,这就与我们的目的相悖了。
# 版本四:Future 解决重复计算
- 将 Map 中的 value 由 V 改成
Future<V>
,这样放进缓存中的就是一个个的 Future 任务了,我们可以通过future.get()
来获取计算的结果。 - put 的时候用
putIfAbsent()
这种原子操作,它只有在没有该 key 的情况下才去 put,不会造成覆盖。返回值是之前的元素,如果是 null 表示之前没有元素。 - 我们在 putIfAbsent 后判断之前是否有元素,如果没有元素,就调用 futureTask.run() 去执行 callable 中的 call() 方法进行计算,然后在方法 return 处用
future.get()
等待计算完成。 - 这样即实现了线程安全,也避免了重复计算。
/**
* 第四代:使用 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
# 版本六:引入缓存过期功能
- 因为计算结果可能会发生改变,我们需要为每个结果指定过期时间,并定期扫描过期的元素,及时更新数据,即使计算结果没有改变也可以减少内存压力。
- 可以利用定时线程池
ScheduledExecutorService
来实现这个功能。 - 如果每个数据的过期时间都一样的话,很容易造成集体过期的情况,所以我们样例中随机生成过期时间。
/**
* 第六代:出于安全性考虑,缓存需要设置有效期
*
* @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