java的cache与future,建立高速缓存机制-java版

前言

​一台计算机的核心是CPU,它是计算机系统的运算和控制核心。由于它处理运算速度快,所以基本都会给CPU配置一级缓存,当CPU要读取一个数据时,首先从缓存中查询,如果没有在从内存或者磁盘块中找。

​同样的,作为一个服务器应用程序,为了让应用程序运行更快速,响应更给力,我们会给它设置一些数据缓存,这样可以提高应用程序的吞吐量、缩短客户端的响应时间。

建立缓存过程分析

​我们从java最常用的方案开始——一个简单的HashMap。

public interface Computable {

V compute(A arg) throws InterruptedException;

}

public class ExpensiveFunction implements Computable {

@Override

public BigInteger compute(String arg) throws InterruptedException {

// after deep thought...

return new BigInteger(arg);

}

}

​Computable接口描述了一个功能,输入类型是A,输出结果的类型是V。ExpensiveFunction实现了Computable。需要花比较长的时间来计算结果。所以我们计划把计算过的值都放进一个HashMap中,这样下一次有同一个A值进来时,直接获取A的计算结果。

2.1 Synchronized版

public class Memoizer1 implements Computable {

private final Map cache = new HashMap();

private final Computable c;

public Memoizer1(Computable c) {

this.c = c;

}

@Override

public synchronized V compute(A arg) throws InterruptedException {

V result = cache.get(arg);

if (result == null) {

result = c.compute(arg);

cache.put(arg, result);

}

return result;

}

}

​Memoizer1实现了第一个版本,HashMap不是线程安全的,所以使用synchronzied关键字来保证线程安全,如果cache变量中有计算结果,直接从cache取,不需要再次计算,省下许多时间。但使用synchronzied使得一次只有一个线程能够执行compute。如果一个线程正在计算结果,那其他调用compute的线程可能被阻塞很长时间,造成性能下降,这不是我们希望通过缓存得到的性能优化结果。

2.2 ConcurrentHashMap版

public class Memoizer2 implements Computable {

private final Map cache = new ConcurrentHashMap<>();

private final Computable c;

public Memoizer2(Computable c) {

this.c = c;

}

@Override

public V compute(A arg) throws InterruptedException {

V result = cache.get(arg);

if (result == null) {

result = c.compute(arg);

cache.put(arg, result);

}

return result;

}

}

​Memoizer2用ConcurrentHashMap取代HashMap,改进了Memoizer1中那种糟糕的并发行为。因为ConcurrentHashMap是线程安全的,所以不需要使用Synchronized关键字,而是使用内部hash桶的分段锁机制。

​Memoizer2与Memoizer1相比,毫无疑问具有了更好的并发性:多线程可以真正并发访问了。但是作为高速缓存仍然存在缺陷:当两个线程同时调用compute时,如果是计算同一个值,此时compute是需要很大的开销的,在一个线程还在计算中时,其它线程不知道,所以可能会重复计算。而我们希望的是:如果A线程正在计算arg,那B就不要重复计算arg,等A计算好,直接取arg对应的V就好了。

2.3 ConcurrentHashMap + FutureTask版

public class Memoizer3 implements Computable {

private final Map> cache = new ConcurrentHashMap<>();

private final Computable c;

public Memoizer3(Computable c) {

this.c = c;

}

@Override

public V compute(A arg) throws InterruptedException {

Future f = cache.get(arg);

if (f == null) {

Callable eval = () -> {

return c.compute(arg);

};

FutureTask ft = new FutureTask<>(eval);

f = ft;

cache.put(arg, ft);

ft.run(); // 调用 c.compute发生在这里

}

try {

return f.get();

} catch (ExecutionException e) {

e.printStackTrace();

throw new RuntimeException(e.getMessage());

}

}

}

​Memoizer3为缓存的值重新定义可存储Map,用ConcurrentHashMap取代ConcurrentHashMap。Memoizer3首先检查一个相应的计算是否开始,如果不是,就创建一个FutureTash,并把它注册到map中,并开始计算,如果是,那么它就会等待正在计算的结果。

​Memoizer3的实现近乎是完美的:它展示了非常好的并发性,能很快返回已经计算过的结果,如果新到的线程请求的是其它线程正在计算的结果,它也会耐心的等待。

​Memoizer3只有一个问题,就是仍然存在这种可能性:2个线程同时计算arg,此时由于compute中的if代码块是非原子性的复合操作,2个线程会同时进入到if代码块中,依旧会同时计算同一个arg。但ConcurrentHashMap中提供了一个原子化的putIfAbsent方法,可以消除Memoizer3的隐患。

2.4 最终版

public class Memoizer implements Computable {

private final Map> cache = new ConcurrentHashMap<>();

private final Computable c;

public Memoizer(Computable c) {

this.c = c;

}

@Override

public V compute(A arg) throws InterruptedException {

Future f = cache.get(arg);

if (f == null) {

Callable eval = () -> {

return c.compute(arg);

};

FutureTask ft = new FutureTask<>(eval);

f = ft;

cache.putIfAbsent(arg, ft);

ft.run(); // 调用 c.compute发生在这里

}

try {

return f.get();

} catch (ExecutionException e) {

e.printStackTrace();

throw new RuntimeException(e.getMessage());

}

}

}

​Memoizer可以说是缓存的完美实现了:支持高并发,同一个参数计算也不会重复执行(多亏于ConcurrentHashMap的putIfAbsent原子化操作)。最终调用者通过调用Future.get(arg)方法获取计算结果。

程序员灯塔

转载请注明原文链接:建立高速缓存机制-java版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值